trickle-observe 0.2.134 → 0.2.136

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.
@@ -1,1966 +0,0 @@
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 AND trace variable assignments —
6
- * the same thing observe-register.ts does for Node's Module._compile,
7
- * 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
- import path from 'path';
21
- import fs from 'fs';
22
- export function tricklePlugin(options = {}) {
23
- const include = options.include
24
- ?? (process.env.TRICKLE_OBSERVE_INCLUDE
25
- ? process.env.TRICKLE_OBSERVE_INCLUDE.split(',').map(s => s.trim())
26
- : []);
27
- const exclude = options.exclude
28
- ?? (process.env.TRICKLE_OBSERVE_EXCLUDE
29
- ? process.env.TRICKLE_OBSERVE_EXCLUDE.split(',').map(s => s.trim())
30
- : []);
31
- const backendUrl = options.backendUrl
32
- ?? process.env.TRICKLE_BACKEND_URL
33
- ?? 'http://localhost:4888';
34
- const debug = options.debug
35
- ?? (process.env.TRICKLE_DEBUG === '1' || process.env.TRICKLE_DEBUG === 'true');
36
- const traceVars = options.traceVars ?? (process.env.TRICKLE_TRACE_VARS !== '0');
37
- function shouldTransform(id) {
38
- // Only JS/TS files
39
- const ext = path.extname(id).toLowerCase();
40
- if (!['.js', '.jsx', '.ts', '.tsx', '.mjs', '.cjs', '.mts'].includes(ext))
41
- return false;
42
- // Skip node_modules
43
- if (id.includes('node_modules'))
44
- return false;
45
- // Skip trickle internals
46
- if (id.includes('trickle-observe') || id.includes('client-js/'))
47
- return false;
48
- // Include filter
49
- if (include.length > 0) {
50
- if (!include.some(p => id.includes(p)))
51
- return false;
52
- }
53
- // Exclude filter
54
- if (exclude.length > 0) {
55
- if (exclude.some(p => id.includes(p)))
56
- return false;
57
- }
58
- return true;
59
- }
60
- return {
61
- name: 'trickle-observe',
62
- enforce: 'post',
63
- configureServer(server) {
64
- // Listen for variable data from browser clients via Vite's HMR WebSocket
65
- const hot = server.hot || server.ws;
66
- if (hot && hot.on) {
67
- const varDir = process.env.TRICKLE_LOCAL_DIR || path.join(process.cwd(), '.trickle');
68
- try {
69
- fs.mkdirSync(varDir, { recursive: true });
70
- }
71
- catch { }
72
- const varsFile = path.join(varDir, 'variables.jsonl');
73
- hot.on('trickle:vars', (data, client) => {
74
- try {
75
- if (data && data.lines) {
76
- fs.appendFileSync(varsFile, data.lines);
77
- }
78
- }
79
- catch { }
80
- });
81
- if (debug) {
82
- console.log(`[trickle/vite] WebSocket bridge active → ${varsFile}`);
83
- }
84
- }
85
- },
86
- transform(code, id, options) {
87
- if (!shouldTransform(id))
88
- return null;
89
- const isSSR = options?.ssr === true;
90
- // Read the original source file to get accurate line numbers.
91
- // Vite transforms the code before our plugin (enforce: 'post'),
92
- // so line numbers from `code` don't match the original .ts file.
93
- let originalSource = null;
94
- try {
95
- originalSource = fs.readFileSync(id, 'utf-8');
96
- }
97
- catch {
98
- // If we can't read the original, we'll use transformed line numbers
99
- }
100
- const moduleName = path.basename(id).replace(/\.[jt]sx?$/, '');
101
- const transformed = transformEsmSource(code, id, moduleName, backendUrl, debug, traceVars, originalSource, isSSR);
102
- if (transformed === code)
103
- return null;
104
- if (debug) {
105
- console.log(`[trickle/vite] Transformed ${moduleName} (${id}) [${isSSR ? 'SSR' : 'browser'}]`);
106
- }
107
- return { code: transformed, map: null };
108
- },
109
- };
110
- }
111
- /**
112
- * Find the opening brace of a function body, skipping the parameter list.
113
- * Starting from the character right after the opening `(` of the parameter list,
114
- * scans forward matching parens to find the closing `)`, then finds the `{` after it.
115
- * Returns -1 if not found.
116
- */
117
- export function findFunctionBodyBrace(source, afterOpenParen) {
118
- let depth = 1;
119
- let pos = afterOpenParen;
120
- // Skip the parameter list (matching parens)
121
- while (pos < source.length && depth > 0) {
122
- const ch = source[pos];
123
- if (ch === '(')
124
- depth++;
125
- else if (ch === ')') {
126
- depth--;
127
- if (depth === 0)
128
- break;
129
- }
130
- else if (ch === '"' || ch === "'" || ch === '`') {
131
- const quote = ch;
132
- pos++;
133
- while (pos < source.length && source[pos] !== quote) {
134
- if (source[pos] === '\\')
135
- pos++;
136
- pos++;
137
- }
138
- }
139
- pos++;
140
- }
141
- // Now find the `{` after the closing `)`
142
- while (pos < source.length) {
143
- const ch = source[pos];
144
- if (ch === '{')
145
- return pos;
146
- if (ch !== ' ' && ch !== '\t' && ch !== '\n' && ch !== '\r' && ch !== ':') {
147
- // Hit something unexpected (like '=>' for arrows, or type annotation chars)
148
- if (ch === '=' && pos + 1 < source.length && source[pos + 1] === '>') {
149
- // Arrow — find { after =>
150
- pos += 2;
151
- continue;
152
- }
153
- // Type annotation — keep going (`: ReturnType`)
154
- }
155
- pos++;
156
- }
157
- return -1;
158
- }
159
- /**
160
- * Find the closing brace position for a function body starting at `openBrace`.
161
- */
162
- function findClosingBrace(source, openBrace) {
163
- let depth = 1;
164
- let pos = openBrace + 1;
165
- while (pos < source.length && depth > 0) {
166
- const ch = source[pos];
167
- if (ch === '{') {
168
- depth++;
169
- }
170
- else if (ch === '}') {
171
- depth--;
172
- if (depth === 0)
173
- return pos;
174
- }
175
- else if (ch === '"' || ch === "'" || ch === '`') {
176
- const quote = ch;
177
- pos++;
178
- while (pos < source.length) {
179
- if (source[pos] === '\\') {
180
- pos++;
181
- }
182
- else if (source[pos] === quote)
183
- break;
184
- else if (quote === '`' && source[pos] === '$' && pos + 1 < source.length && source[pos + 1] === '{') {
185
- pos += 2;
186
- let tDepth = 1;
187
- while (pos < source.length && tDepth > 0) {
188
- if (source[pos] === '{')
189
- tDepth++;
190
- else if (source[pos] === '}')
191
- tDepth--;
192
- pos++;
193
- }
194
- continue;
195
- }
196
- pos++;
197
- }
198
- }
199
- else if (ch === '/' && pos + 1 < source.length && source[pos + 1] === '/') {
200
- while (pos < source.length && source[pos] !== '\n')
201
- pos++;
202
- }
203
- else if (ch === '/' && pos + 1 < source.length && source[pos + 1] === '*') {
204
- pos += 2;
205
- while (pos < source.length - 1 && !(source[pos] === '*' && source[pos + 1] === '/'))
206
- pos++;
207
- pos++;
208
- }
209
- pos++;
210
- }
211
- return -1;
212
- }
213
- /**
214
- * Find the matching closing paren for an opening paren at openParen.
215
- * JSX-safe version: counts `(` and `)` depth, handles JSX curly expressions,
216
- * but does NOT treat `'` as a string delimiter (apostrophes in JSX text content
217
- * like `I'm` would incorrectly consume parens). Double-quoted strings inside
218
- * JSX attr values typically contain balanced parens so can be ignored safely.
219
- */
220
- function findMatchingParen(source, openParen) {
221
- let depth = 1;
222
- let pos = openParen + 1;
223
- while (pos < source.length && depth > 0) {
224
- const ch = source[pos];
225
- if (ch === '(') {
226
- depth++;
227
- }
228
- else if (ch === ')') {
229
- depth--;
230
- if (depth === 0)
231
- return pos;
232
- }
233
- // Skip JSX expression blocks {expr} — not parens but skip curly content to avoid
234
- // false paren matches inside template expressions
235
- pos++;
236
- }
237
- return -1;
238
- }
239
- /**
240
- * Find variable declarations in source and return insertions for tracing.
241
- * Handles: const x = ...; let x = ...; var x = ...;
242
- * Skips: destructuring, for-loop vars, require() calls, imports, type annotations.
243
- */
244
- function findVarDeclarations(source) {
245
- const varInsertions = [];
246
- // Match: const/let/var <identifier> = <something>
247
- const varRegex = /^([ \t]*)(export\s+)?(const|let|var)\s+([a-zA-Z_$][a-zA-Z0-9_$]*)\s*(?::\s*[^=]+?)?\s*=[^=]/gm;
248
- let vmatch;
249
- while ((vmatch = varRegex.exec(source)) !== null) {
250
- const varName = vmatch[4];
251
- // Skip trickle internals
252
- if (varName.startsWith('__trickle'))
253
- continue;
254
- // Skip TS compiled vars and helpers
255
- if (varName === '_a' || varName === '_b' || varName === '_c')
256
- continue;
257
- if (varName === '__createBinding' || varName === '__setModuleDefault' || varName === '__importStar' || varName === '__importDefault')
258
- continue;
259
- if (varName === '__decorate' || varName === '__metadata' || varName === '__param' || varName === '__awaiter')
260
- continue;
261
- if (varName === 'ownKeys' || varName === 'desc')
262
- continue;
263
- // Skip esbuild helpers
264
- if (varName === '__defProp' || varName === '__defNormalProp' || varName === '__publicField' || varName === '__getOwnPropNames')
265
- continue;
266
- if (varName === '__commonJS' || varName === '__toCommonJS' || varName === '__export' || varName === '__copyProps')
267
- continue;
268
- // Skip webpack internals
269
- if (varName.startsWith('__webpack_'))
270
- continue;
271
- if (varName === '__unused_webpack_module')
272
- continue;
273
- // Skip React Refresh / HMR internals (Vite, webpack, Next.js inject these)
274
- if (varName === 'prevRefreshReg' || varName === 'prevRefreshSig' || varName === 'inWebWorker' || varName === 'invalidateMessage')
275
- continue;
276
- if (varName === '_s' || varName === '_c2' || varName === '_s2')
277
- continue;
278
- // Skip single-underscore discard variables
279
- if (varName === '_')
280
- continue;
281
- // Check if this is a require() call or import — skip those
282
- const restOfLine = source.slice(vmatch.index + vmatch[0].length - 1, vmatch.index + vmatch[0].length + 200);
283
- if (/^\s*require\s*\(/.test(restOfLine))
284
- continue;
285
- // Skip function/class assignments (those are handled by function wrapping)
286
- if (/^\s*(?:async\s+)?(?:function\s|\([^)]*\)\s*(?::\s*[^=]+?)?\s*=>|\w+\s*=>)/.test(restOfLine))
287
- continue;
288
- // Calculate line number
289
- let lineNo = 1;
290
- for (let i = 0; i < vmatch.index; i++) {
291
- if (source[i] === '\n')
292
- lineNo++;
293
- }
294
- // Find the end of this statement
295
- const startPos = vmatch.index + vmatch[0].length - 1;
296
- let pos = startPos;
297
- let depth = 0;
298
- let foundEnd = -1;
299
- while (pos < source.length) {
300
- const ch = source[pos];
301
- if (ch === '(' || ch === '[' || ch === '{') {
302
- depth++;
303
- }
304
- else if (ch === ')' || ch === ']' || ch === '}') {
305
- depth--;
306
- if (depth < 0)
307
- break;
308
- }
309
- else if (ch === ';' && depth === 0) {
310
- foundEnd = pos;
311
- break;
312
- }
313
- else if (ch === '\n' && depth === 0) {
314
- const nextNonWs = source.slice(pos + 1).match(/^\s*(\S)/);
315
- if (nextNonWs && !'.+=-|&?:,'.includes(nextNonWs[1])) {
316
- foundEnd = pos;
317
- break;
318
- }
319
- }
320
- else if (ch === '"' || ch === "'" || ch === '`') {
321
- const quote = ch;
322
- pos++;
323
- while (pos < source.length) {
324
- if (source[pos] === '\\') {
325
- pos++;
326
- }
327
- else if (source[pos] === quote)
328
- break;
329
- pos++;
330
- }
331
- }
332
- else if (ch === '/' && pos + 1 < source.length && source[pos + 1] === '/') {
333
- while (pos < source.length && source[pos] !== '\n')
334
- pos++;
335
- continue;
336
- }
337
- else if (ch === '/' && pos + 1 < source.length && source[pos + 1] === '*') {
338
- pos += 2;
339
- while (pos < source.length - 1 && !(source[pos] === '*' && source[pos + 1] === '/'))
340
- pos++;
341
- pos++;
342
- }
343
- pos++;
344
- }
345
- if (foundEnd === -1)
346
- continue;
347
- varInsertions.push({ lineEnd: foundEnd + 1, varName, lineNo });
348
- }
349
- return varInsertions;
350
- }
351
- /**
352
- * Find destructured variable declarations: const { a, b } = ... and const [a, b] = ...
353
- * Extracts the individual variable names from the destructuring pattern.
354
- */
355
- function findDestructuredDeclarations(source) {
356
- const results = [];
357
- // Match: const/let/var { ... } = ... or const/let/var [ ... ] = ...
358
- const destructRegex = /^[ \t]*(?:export\s+)?(?:const|let|var)\s+(\{[^}]*\}|\[[^\]]*\])\s*(?::\s*[^=]+?)?\s*=[^=]/gm;
359
- let match;
360
- while ((match = destructRegex.exec(source)) !== null) {
361
- const pattern = match[1];
362
- // Extract variable names from the destructuring pattern
363
- const varNames = extractDestructuredNames(pattern);
364
- if (varNames.length === 0)
365
- continue;
366
- // Skip if it's a require() call
367
- const restOfLine = source.slice(match.index + match[0].length - 1, match.index + match[0].length + 200);
368
- if (/^\s*require\s*\(/.test(restOfLine))
369
- continue;
370
- // Calculate line number
371
- let lineNo = 1;
372
- for (let i = 0; i < match.index; i++) {
373
- if (source[i] === '\n')
374
- lineNo++;
375
- }
376
- // Find the end of this statement (same logic as findVarDeclarations)
377
- const startPos = match.index + match[0].length - 1;
378
- let pos = startPos;
379
- let depth = 0;
380
- let foundEnd = -1;
381
- while (pos < source.length) {
382
- const ch = source[pos];
383
- if (ch === '(' || ch === '[' || ch === '{') {
384
- depth++;
385
- }
386
- else if (ch === ')' || ch === ']' || ch === '}') {
387
- depth--;
388
- if (depth < 0)
389
- break;
390
- }
391
- else if (ch === ';' && depth === 0) {
392
- foundEnd = pos;
393
- break;
394
- }
395
- else if (ch === '\n' && depth === 0) {
396
- const nextNonWs = source.slice(pos + 1).match(/^\s*(\S)/);
397
- if (nextNonWs && !'.+=-|&?:,'.includes(nextNonWs[1])) {
398
- foundEnd = pos;
399
- break;
400
- }
401
- }
402
- else if (ch === '"' || ch === "'" || ch === '`') {
403
- const quote = ch;
404
- pos++;
405
- while (pos < source.length) {
406
- if (source[pos] === '\\') {
407
- pos++;
408
- }
409
- else if (source[pos] === quote)
410
- break;
411
- pos++;
412
- }
413
- }
414
- else if (ch === '/' && pos + 1 < source.length && source[pos + 1] === '/') {
415
- while (pos < source.length && source[pos] !== '\n')
416
- pos++;
417
- continue;
418
- }
419
- else if (ch === '/' && pos + 1 < source.length && source[pos + 1] === '*') {
420
- pos += 2;
421
- while (pos < source.length - 1 && !(source[pos] === '*' && source[pos + 1] === '/'))
422
- pos++;
423
- pos++;
424
- }
425
- pos++;
426
- }
427
- if (foundEnd === -1)
428
- continue;
429
- results.push({ lineEnd: foundEnd + 1, varNames, lineNo });
430
- }
431
- return results;
432
- }
433
- /**
434
- * Extract variable names from a destructuring pattern.
435
- * Handles: { a, b, c: d } → ['a', 'b', 'd'] (renamed vars use the local name)
436
- * Handles: [a, b, ...rest] → ['a', 'b', 'rest']
437
- * Handles: { a: { b, c } } → ['b', 'c'] (nested destructuring)
438
- */
439
- function extractDestructuredNames(pattern) {
440
- const names = [];
441
- // Remove outer braces/brackets
442
- const inner = pattern.slice(1, -1).trim();
443
- if (!inner)
444
- return names;
445
- // Split by commas at depth 0
446
- const parts = [];
447
- let depth = 0;
448
- let current = '';
449
- for (const ch of inner) {
450
- if (ch === '{' || ch === '[')
451
- depth++;
452
- else if (ch === '}' || ch === ']')
453
- depth--;
454
- else if (ch === ',' && depth === 0) {
455
- parts.push(current.trim());
456
- current = '';
457
- continue;
458
- }
459
- current += ch;
460
- }
461
- if (current.trim())
462
- parts.push(current.trim());
463
- for (let part of parts) {
464
- // Remove type annotations: `a: Type` vs `a: b` (rename)
465
- // Skip rest elements for now: ...rest → rest
466
- if (part.startsWith('...')) {
467
- const restName = part.slice(3).trim().split(/[\s:]/)[0];
468
- if (/^[a-zA-Z_$][a-zA-Z0-9_$]*$/.test(restName)) {
469
- names.push(restName);
470
- }
471
- continue;
472
- }
473
- // Check for rename pattern: key: localName or key: { nested }
474
- const colonIdx = part.indexOf(':');
475
- if (colonIdx !== -1) {
476
- const afterColon = part.slice(colonIdx + 1).trim();
477
- // Nested destructuring: key: { a, b } or key: [a, b]
478
- if (afterColon.startsWith('{') || afterColon.startsWith('[')) {
479
- const nestedNames = extractDestructuredNames(afterColon);
480
- names.push(...nestedNames);
481
- }
482
- else {
483
- // Rename: key: localName — extract localName (skip if it has another colon for type annotation)
484
- const localName = afterColon.split(/[\s=]/)[0].trim();
485
- if (/^[a-zA-Z_$][a-zA-Z0-9_$]*$/.test(localName)) {
486
- names.push(localName);
487
- }
488
- }
489
- }
490
- else {
491
- // Simple: just the identifier (possibly with default: `a = defaultVal`)
492
- const name = part.split(/[\s=]/)[0].trim();
493
- if (/^[a-zA-Z_$][a-zA-Z0-9_$]*$/.test(name)) {
494
- names.push(name);
495
- }
496
- }
497
- }
498
- return names;
499
- }
500
- /**
501
- * Find import declarations and extract imported bindings for tracing.
502
- * Handles:
503
- * import { a, b } from '...' → trace a, b
504
- * import { a as b } from '...' → trace b (local name)
505
- * import X from '...' → trace X (default import)
506
- * import * as X from '...' → trace X (namespace import)
507
- * import X, { a, b } from '...' → trace X, a, b
508
- * Returns insertions to place AFTER the import statement.
509
- */
510
- function findImportDeclarations(source) {
511
- const results = [];
512
- // Match import statements (potentially multiline)
513
- // We scan for `import` at the start of a line (with optional whitespace)
514
- const importRegex = /^[ \t]*import\s+/gm;
515
- let match;
516
- while ((match = importRegex.exec(source)) !== null) {
517
- const importStart = match.index;
518
- let pos = importStart + match[0].length;
519
- const varNames = [];
520
- // Skip type-only imports: `import type ...`
521
- if (source.slice(pos).startsWith('type ') || source.slice(pos).startsWith('type{'))
522
- continue;
523
- // Skip bare imports: `import '...'` or `import "..."`
524
- const afterImport = source[pos];
525
- if (afterImport === '"' || afterImport === "'")
526
- continue;
527
- // Parse the import clause
528
- // Could be:
529
- // * as X
530
- // X (default)
531
- // { a, b, c as d }
532
- // X, { a, b }
533
- // X, * as Y
534
- // Check for namespace import: * as X
535
- if (source[pos] === '*') {
536
- const nsMatch = source.slice(pos).match(/^\*\s+as\s+([a-zA-Z_$][a-zA-Z0-9_$]*)/);
537
- if (nsMatch) {
538
- varNames.push(nsMatch[1]);
539
- pos += nsMatch[0].length;
540
- }
541
- }
542
- // Check for default import or named imports
543
- else if (source[pos] === '{') {
544
- // Named imports only: { a, b, c as d }
545
- const closeIdx = source.indexOf('}', pos);
546
- if (closeIdx === -1)
547
- continue;
548
- const namedStr = source.slice(pos + 1, closeIdx);
549
- const names = parseNamedImports(namedStr);
550
- varNames.push(...names);
551
- pos = closeIdx + 1;
552
- }
553
- else {
554
- // Default import: X or X, { ... } or X, * as Y
555
- const defaultMatch = source.slice(pos).match(/^([a-zA-Z_$][a-zA-Z0-9_$]*)/);
556
- if (defaultMatch) {
557
- varNames.push(defaultMatch[1]);
558
- pos += defaultMatch[0].length;
559
- // Skip whitespace and comma
560
- while (pos < source.length && /[\s,]/.test(source[pos]))
561
- pos++;
562
- // Check for additional named imports: , { a, b }
563
- if (source[pos] === '{') {
564
- const closeIdx = source.indexOf('}', pos);
565
- if (closeIdx !== -1) {
566
- const namedStr = source.slice(pos + 1, closeIdx);
567
- const names = parseNamedImports(namedStr);
568
- varNames.push(...names);
569
- pos = closeIdx + 1;
570
- }
571
- }
572
- // Check for namespace: , * as Y
573
- else if (source[pos] === '*') {
574
- const nsMatch = source.slice(pos).match(/^\*\s+as\s+([a-zA-Z_$][a-zA-Z0-9_$]*)/);
575
- if (nsMatch) {
576
- varNames.push(nsMatch[1]);
577
- pos += nsMatch[0].length;
578
- }
579
- }
580
- }
581
- }
582
- if (varNames.length === 0)
583
- continue;
584
- // Skip trickle internals
585
- const filtered = varNames.filter(n => !n.startsWith('__trickle'));
586
- if (filtered.length === 0)
587
- continue;
588
- // Find the end of the import statement (semicolon or newline after `from '...'`)
589
- const fromIdx = source.indexOf('from', pos);
590
- if (fromIdx === -1)
591
- continue;
592
- // Find the end: either `;` or end of line after the string literal
593
- let endPos = fromIdx + 4;
594
- // Skip whitespace
595
- while (endPos < source.length && /\s/.test(source[endPos]))
596
- endPos++;
597
- // Skip the string literal
598
- if (endPos < source.length && (source[endPos] === '"' || source[endPos] === "'")) {
599
- const quote = source[endPos];
600
- endPos++;
601
- while (endPos < source.length && source[endPos] !== quote) {
602
- if (source[endPos] === '\\')
603
- endPos++;
604
- endPos++;
605
- }
606
- endPos++; // skip closing quote
607
- }
608
- // Skip optional semicolon
609
- while (endPos < source.length && (source[endPos] === ';' || source[endPos] === ' ' || source[endPos] === '\t'))
610
- endPos++;
611
- // Calculate line number
612
- let lineNo = 1;
613
- for (let i = 0; i < importStart; i++) {
614
- if (source[i] === '\n')
615
- lineNo++;
616
- }
617
- results.push({ lineEnd: endPos, varNames: filtered, lineNo });
618
- }
619
- return results;
620
- }
621
- /**
622
- * Parse named imports from the content between { and }.
623
- * Handles: a, b, c as d, type e (skips type-only imports)
624
- * Returns local binding names.
625
- */
626
- function parseNamedImports(namedStr) {
627
- const names = [];
628
- const parts = namedStr.split(',');
629
- for (const part of parts) {
630
- const trimmed = part.trim();
631
- if (!trimmed)
632
- continue;
633
- // Skip type-only: `type Foo` or `type Foo as Bar`
634
- if (/^type\s+/.test(trimmed))
635
- continue;
636
- // Check for alias: `original as local`
637
- const asMatch = trimmed.match(/^[a-zA-Z_$][a-zA-Z0-9_$]*\s+as\s+([a-zA-Z_$][a-zA-Z0-9_$]*)/);
638
- if (asMatch) {
639
- names.push(asMatch[1]);
640
- }
641
- else {
642
- const name = trimmed.split(/[\s]/)[0];
643
- if (/^[a-zA-Z_$][a-zA-Z0-9_$]*$/.test(name)) {
644
- names.push(name);
645
- }
646
- }
647
- }
648
- return names;
649
- }
650
- /**
651
- * Find class body ranges in source code. Handles both:
652
- * class Foo { ... }
653
- * var Foo = class { ... }
654
- * Returns an array of [start, end] positions (inclusive of braces).
655
- */
656
- export function findClassBodyRanges(source) {
657
- const ranges = [];
658
- // Match both class declarations and class expressions
659
- const classRegex = /\bclass\s*(?:[a-zA-Z_$][a-zA-Z0-9_$]*)?\s*(?:extends\s+[a-zA-Z_$.[\]]+\s*)?\{/g;
660
- let m;
661
- while ((m = classRegex.exec(source)) !== null) {
662
- const openBrace = source.indexOf('{', m.index + 5); // skip past 'class'
663
- if (openBrace === -1)
664
- continue;
665
- // Find matching close brace
666
- let depth = 1;
667
- let pos = openBrace + 1;
668
- while (pos < source.length && depth > 0) {
669
- const ch = source[pos];
670
- if (ch === '{')
671
- depth++;
672
- else if (ch === '}') {
673
- depth--;
674
- if (depth === 0)
675
- break;
676
- }
677
- else if (ch === '"' || ch === "'" || ch === '`') {
678
- const q = ch;
679
- pos++;
680
- while (pos < source.length) {
681
- if (source[pos] === '\\')
682
- pos++;
683
- else if (source[pos] === q)
684
- break;
685
- else if (q === '`' && source[pos] === '$' && source[pos + 1] === '{') {
686
- pos += 2;
687
- let td = 1;
688
- while (pos < source.length && td > 0) {
689
- if (source[pos] === '{')
690
- td++;
691
- else if (source[pos] === '}')
692
- td--;
693
- pos++;
694
- }
695
- continue;
696
- }
697
- pos++;
698
- }
699
- }
700
- else if (ch === '/' && source[pos + 1] === '/') {
701
- while (pos < source.length && source[pos] !== '\n')
702
- pos++;
703
- }
704
- else if (ch === '/' && source[pos + 1] === '*') {
705
- pos += 2;
706
- while (pos < source.length - 1 && !(source[pos] === '*' && source[pos + 1] === '/'))
707
- pos++;
708
- pos++;
709
- }
710
- pos++;
711
- }
712
- if (depth === 0)
713
- ranges.push([openBrace, pos]);
714
- }
715
- return ranges;
716
- }
717
- /**
718
- * Find variable reassignments (not declarations) and return insertions for tracing.
719
- * Handles: x = newValue; x += 1; x ||= fallback; etc.
720
- * Only matches standalone reassignment statements at the start of a line.
721
- * Skips: property assignments (obj.x = ...), indexed (arr[i] = ...),
722
- * comparisons (===, !==), arrow functions (=>), declarations (const/let/var).
723
- */
724
- export function findReassignments(source) {
725
- const results = [];
726
- // Pre-compute class body ranges to skip class field declarations
727
- const classRanges = findClassBodyRanges(source);
728
- // Match: <identifier> <assignOp>= <value> at the start of a line
729
- // Compound operators: +=, -=, *=, /=, %=, **=, &&=, ||=, ??=, <<=, >>=, >>>=, &=, |=, ^=
730
- // Plain: = (but not ==, ===, =>, !=)
731
- const reassignRegex = /^([ \t]*)([a-zA-Z_$][a-zA-Z0-9_$]*)\s*(?:\+|-|\*\*?|\/|%|&&|\|\||<<|>>>?|&|\||\^|\?\?)?=[^=>]/gm;
732
- let match;
733
- while ((match = reassignRegex.exec(source)) !== null) {
734
- const varName = match[2];
735
- // Skip trickle internals
736
- if (varName.startsWith('__trickle') || varName.startsWith('_$'))
737
- continue;
738
- // Skip common non-variable patterns
739
- if (varName === '_a' || varName === '_b' || varName === '_c')
740
- continue;
741
- // Skip 'this', 'self', 'super' (not reassignable in practice)
742
- if (varName === 'this' || varName === 'super')
743
- continue;
744
- // Skip TS compiler helpers and module internals
745
- if (varName === 'ownKeys' || varName === 'desc')
746
- continue;
747
- // Skip React Refresh / HMR internals and discard variables
748
- if (varName === 'prevRefreshReg' || varName === 'prevRefreshSig' || varName === 'inWebWorker')
749
- continue;
750
- if (varName === '_s' || varName === '_c2' || varName === '_s2' || varName === '_')
751
- continue;
752
- // Skip keywords that could look like identifiers
753
- if (['if', 'else', 'while', 'for', 'do', 'switch', 'case', 'default', 'return', 'throw',
754
- 'break', 'continue', 'try', 'catch', 'finally', 'new', 'delete', 'typeof', 'void',
755
- 'yield', 'await', 'class', 'extends', 'import', 'export', 'from', 'as', 'of', 'in',
756
- 'const', 'let', 'var', 'function', 'true', 'false', 'null', 'undefined'].includes(varName))
757
- continue;
758
- // Check that this line doesn't start with const/let/var (would be a declaration, already handled)
759
- const lineStart = source.lastIndexOf('\n', match.index) + 1;
760
- const linePrefix = source.slice(lineStart, match.index + match[1].length).trim();
761
- if (/^(export\s+)?(const|let|var)\s/.test(source.slice(lineStart).trimStart()))
762
- continue;
763
- // Skip class field declarations (e.g., `tasks = []` inside a class body)
764
- // Inserting trace calls inside class bodies causes SyntaxError
765
- if (classRanges.some(([start, end]) => match.index > start && match.index < end))
766
- continue;
767
- // Skip if this looks like a property in an object literal (preceded by a key: pattern on same line)
768
- // or if it's a label (label: ...)
769
- const beforeOnLine = source.slice(lineStart, match.index).trim();
770
- if (beforeOnLine.endsWith(':') || beforeOnLine.endsWith(','))
771
- continue;
772
- // Skip comma-separated multi-variable declaration continuations:
773
- // var X = 'foo',
774
- // Y = 'bar'; ← Y looks like a reassignment but is actually a declaration
775
- // Detect by checking if the previous non-empty line ends with ','
776
- if (beforeOnLine.length === 0) {
777
- const prevLineEnd = source.lastIndexOf('\n', lineStart - 1);
778
- if (prevLineEnd >= 0) {
779
- const prevLine = source.slice(source.lastIndexOf('\n', prevLineEnd - 1) + 1, prevLineEnd).trimEnd();
780
- if (prevLine.endsWith(','))
781
- continue;
782
- }
783
- }
784
- // Calculate line number
785
- let lineNo = 1;
786
- for (let i = 0; i < match.index; i++) {
787
- if (source[i] === '\n')
788
- lineNo++;
789
- }
790
- // Find end of statement
791
- const startPos = match.index + match[0].length - 1;
792
- let pos = startPos;
793
- let depth = 0;
794
- let foundEnd = -1;
795
- while (pos < source.length) {
796
- const ch = source[pos];
797
- if (ch === '(' || ch === '[' || ch === '{') {
798
- depth++;
799
- }
800
- else if (ch === ')' || ch === ']' || ch === '}') {
801
- depth--;
802
- if (depth < 0)
803
- break;
804
- }
805
- else if (ch === ';' && depth === 0) {
806
- foundEnd = pos;
807
- break;
808
- }
809
- else if (ch === '\n' && depth === 0) {
810
- const nextNonWs = source.slice(pos + 1).match(/^\s*(\S)/);
811
- if (nextNonWs && !'.+=-|&?:,'.includes(nextNonWs[1])) {
812
- // Check if a recent non-empty line ends with an operator
813
- let checkPos2 = pos;
814
- let lastCh2 = '';
815
- for (let back = 0; back < 5; back++) {
816
- const prevNL2 = source.lastIndexOf('\n', checkPos2 - 1);
817
- const prevLine2 = source.slice(prevNL2 + 1, checkPos2).trimEnd();
818
- if (prevLine2.length > 0) {
819
- lastCh2 = prevLine2[prevLine2.length - 1];
820
- break;
821
- }
822
- checkPos2 = prevNL2;
823
- if (prevNL2 <= 0)
824
- break;
825
- }
826
- if (lastCh2 && '=+-*/%&|^~<>?:,({['.includes(lastCh2)) {
827
- pos++;
828
- continue;
829
- }
830
- foundEnd = pos;
831
- break;
832
- }
833
- }
834
- else if (ch === '"' || ch === "'" || ch === '`') {
835
- const quote = ch;
836
- pos++;
837
- while (pos < source.length) {
838
- if (source[pos] === '\\') {
839
- pos++;
840
- }
841
- else if (source[pos] === quote)
842
- break;
843
- pos++;
844
- }
845
- }
846
- else if (ch === '/' && pos + 1 < source.length && source[pos + 1] !== '/' && source[pos + 1] !== '*') {
847
- // Possible regex literal
848
- let rp = pos - 1;
849
- while (rp >= 0 && (source[rp] === ' ' || source[rp] === '\t'))
850
- rp--;
851
- const rpCh = rp >= 0 ? source[rp] : '';
852
- if ('=(!,;:?[{&|^~+-><%'.includes(rpCh) || source.slice(Math.max(0, rp - 5), rp + 1).match(/\b(return|typeof|instanceof|in|of|void|delete|throw|new|case)\s*$/)) {
853
- pos++;
854
- while (pos < source.length) {
855
- if (source[pos] === '\\')
856
- pos++;
857
- else if (source[pos] === '[') {
858
- pos++;
859
- while (pos < source.length && source[pos] !== ']') {
860
- if (source[pos] === '\\')
861
- pos++;
862
- pos++;
863
- }
864
- }
865
- else if (source[pos] === '/')
866
- break;
867
- pos++;
868
- }
869
- while (pos + 1 < source.length && /[gimsuy]/.test(source[pos + 1]))
870
- pos++;
871
- }
872
- }
873
- else if (ch === '/' && pos + 1 < source.length && source[pos + 1] === '/') {
874
- while (pos < source.length && source[pos] !== '\n')
875
- pos++;
876
- continue;
877
- }
878
- else if (ch === '/' && pos + 1 < source.length && source[pos + 1] === '*') {
879
- pos += 2;
880
- while (pos < source.length - 1 && !(source[pos] === '*' && source[pos + 1] === '/'))
881
- pos++;
882
- pos++;
883
- }
884
- pos++;
885
- }
886
- if (foundEnd === -1)
887
- continue;
888
- results.push({ lineEnd: foundEnd + 1, varName, lineNo });
889
- }
890
- return results;
891
- }
892
- /**
893
- * Find for-loop variable declarations and return insertions for tracing.
894
- * Handles:
895
- * for (const item of items) { ... } → trace item
896
- * for (const [key, val] of entries) { ... } → trace key, val
897
- * for (const { a, b } of items) { ... } → trace a, b
898
- * for (const key in obj) { ... } → trace key
899
- * for (let i = 0; i < n; i++) { ... } → trace i
900
- * Inserts trace calls at the start of the loop body.
901
- */
902
- /**
903
- * Find catch clause variables and return insertions for tracing.
904
- * Handles: catch (err) { ... } → trace err at start of catch body.
905
- */
906
- export function findCatchVars(source) {
907
- const results = [];
908
- const catchRegex = /\bcatch\s*\(\s*([a-zA-Z_$][a-zA-Z0-9_$]*)\s*(?::\s*[^)]+?)?\s*\)\s*\{/g;
909
- let match;
910
- while ((match = catchRegex.exec(source)) !== null) {
911
- const varName = match[1];
912
- if (varName.startsWith('__trickle'))
913
- continue;
914
- const bodyBrace = match.index + match[0].length - 1;
915
- let lineNo = 1;
916
- for (let i = 0; i < match.index; i++) {
917
- if (source[i] === '\n')
918
- lineNo++;
919
- }
920
- results.push({ bodyStart: bodyBrace + 1, varNames: [varName], lineNo });
921
- }
922
- return results;
923
- }
924
- /**
925
- * Find simple JSX text expressions and return insertions for tracing.
926
- * Only traces simple expressions in text content positions (after > or between tags):
927
- * <p>{count}</p> → trace count
928
- * <span>{user.name}</span> → trace user.name
929
- * <div>{a + b}</div> → trace a + b (simple binary)
930
- * <p>{x ? 'a' : 'b'}</p> → trace ternary
931
- * Skips: attribute expressions, .map() calls, JSX elements, spread, complex calls.
932
- * Uses comma operator: {(__trickle_tv(expr, name, line), expr)} — safe, returns original value.
933
- */
934
- function findJsxExpressions(source) {
935
- const results = [];
936
- // Find JSX text expressions: characters `>` followed (with optional whitespace/text) by `{expr}`
937
- // We look for `{` that follows `>` (possibly with whitespace or text between)
938
- // and is NOT preceded by `=` (which would be an attribute value)
939
- const jsxExprRegex = /\{/g;
940
- let match;
941
- while ((match = jsxExprRegex.exec(source)) !== null) {
942
- const bracePos = match.index;
943
- // Skip if this `{` is part of an import statement: `import { ... } from '...'`
944
- const lineStart = source.lastIndexOf('\n', bracePos - 1) + 1;
945
- const linePrefix = source.slice(lineStart, bracePos).trimStart();
946
- if (/^import\s/.test(linePrefix) || /^import\s/.test(linePrefix.replace(/^export\s+/, '')))
947
- continue;
948
- // Skip if inside a string or comment
949
- // Simple check: look at the character before `{`
950
- const charBefore = bracePos > 0 ? source[bracePos - 1] : '';
951
- // Skip if this is an attribute value: preceded by `=` (with optional whitespace)
952
- const beforeSlice = source.slice(Math.max(0, bracePos - 5), bracePos).trimEnd();
953
- if (beforeSlice.endsWith('='))
954
- continue;
955
- // Skip if this looks like a template literal expression `${`
956
- if (charBefore === '$')
957
- continue;
958
- // Skip if preceded (past whitespace) by `,`, `(`, or `:` — function arguments, object literals, attribute values
959
- if (charBefore === ',')
960
- continue;
961
- {
962
- let scanBack = bracePos - 1;
963
- while (scanBack >= 0 && (source[scanBack] === ' ' || source[scanBack] === '\t' || source[scanBack] === '\n' || source[scanBack] === '\r'))
964
- scanBack--;
965
- if (scanBack >= 0 && (source[scanBack] === ',' || source[scanBack] === '(' || source[scanBack] === ':' || source[scanBack] === ')'))
966
- continue;
967
- }
968
- // Skip if this `{` is part of a variable declaration destructuring: `const { ... }` or `let { ... }`
969
- if (/(?:const|let|var)\s*$/.test(source.slice(Math.max(0, bracePos - 10), bracePos)))
970
- continue;
971
- // Must be in a JSX context: look backward for `>` or `}` (closing tag bracket or prev expression)
972
- // before hitting structural JS characters like `{`, `(`, `;`
973
- let inJsx = false;
974
- let scanPos = bracePos - 1;
975
- while (scanPos >= 0) {
976
- const ch = source[scanPos];
977
- if (ch === '>') {
978
- inJsx = true;
979
- break;
980
- }
981
- if (ch === '}') {
982
- inJsx = true;
983
- break;
984
- } // After a previous JSX expression
985
- if (ch === '{' || ch === ';')
986
- break;
987
- // `(` breaks scan in code context, but in JSX text `(` is normal
988
- // Check: if `(` is preceded by `>` or text, it's JSX text
989
- if (ch === '(') {
990
- const before = source.slice(Math.max(0, scanPos - 20), scanPos).trim();
991
- if (before.endsWith('>') || /[a-zA-Z0-9\s]$/.test(before)) {
992
- // Could be JSX text like "Users ({count})" — keep scanning
993
- scanPos--;
994
- continue;
995
- }
996
- break;
997
- }
998
- // `=` only breaks if NOT preceded by other text (could be JSX text like "count = 5")
999
- if (ch === '=' && scanPos > 0 && /\s/.test(source[scanPos - 1])) {
1000
- // Check if this `=` is a JSX attribute assignment: look further back for tag
1001
- const attrCheck = source.slice(Math.max(0, scanPos - 30), scanPos).trim();
1002
- if (/^[a-zA-Z]/.test(attrCheck))
1003
- break; // Likely an attribute
1004
- }
1005
- if (ch === '\n') {
1006
- // Check context of previous lines
1007
- const lineAbove = source.slice(Math.max(0, scanPos - 100), scanPos);
1008
- if (/<|>|\}/.test(lineAbove)) {
1009
- inJsx = true;
1010
- break;
1011
- }
1012
- break;
1013
- }
1014
- scanPos--;
1015
- }
1016
- if (!inJsx)
1017
- continue;
1018
- // Find the matching closing `}` for this expression
1019
- let depth = 1;
1020
- let pos = bracePos + 1;
1021
- while (pos < source.length && depth > 0) {
1022
- const ch = source[pos];
1023
- if (ch === '{')
1024
- depth++;
1025
- else if (ch === '}')
1026
- depth--;
1027
- else if (ch === '"' || ch === "'" || ch === '`') {
1028
- const q = ch;
1029
- pos++;
1030
- while (pos < source.length && source[pos] !== q) {
1031
- if (source[pos] === '\\')
1032
- pos++;
1033
- pos++;
1034
- }
1035
- }
1036
- pos++;
1037
- }
1038
- if (depth !== 0)
1039
- continue;
1040
- const exprEnd = pos - 1; // position of closing `}`
1041
- const exprText = source.slice(bracePos + 1, exprEnd).trim();
1042
- // Skip empty expressions
1043
- if (!exprText)
1044
- continue;
1045
- // Skip complex expressions that we don't want to trace:
1046
- // - JSX elements: contains `<` (could be a component)
1047
- // - .map/.filter/.reduce calls (return arrays of elements)
1048
- // - Spread: starts with `...`
1049
- // - Arrow functions: contains `=>`
1050
- // - Already traced: starts with `__trickle`
1051
- if (exprText.includes('<') || exprText.includes('=>'))
1052
- continue;
1053
- if (exprText.startsWith('...'))
1054
- continue;
1055
- if (exprText.startsWith('__trickle'))
1056
- continue;
1057
- if (/\.(map|filter|reduce|forEach|flatMap)\s*\(/.test(exprText))
1058
- continue;
1059
- // Skip function calls with parens (complex expressions) — but allow property access
1060
- // Allow: user.name, count, x ? 'a' : 'b', a + b
1061
- // Skip: fn(), obj.method(), Component()
1062
- if (/\w\s*\(/.test(exprText) && !exprText.includes('?'))
1063
- continue;
1064
- // Only trace if it's a "simple" expression:
1065
- // - Identifier: count, name
1066
- // - Property access: user.name, item.price.formatted
1067
- // - Simple ternary: x ? 'a' : 'b'
1068
- // - Simple arithmetic: count * 2, price + tax
1069
- const isSimple = /^[a-zA-Z_$][a-zA-Z0-9_$]*(\.[a-zA-Z_$][a-zA-Z0-9_$]*)*$/.test(exprText) || // identifier/property
1070
- /^[a-zA-Z_$]/.test(exprText); // starts with identifier (covers ternary, arithmetic)
1071
- if (!isSimple)
1072
- continue;
1073
- // Calculate line number
1074
- let lineNo = 1;
1075
- for (let i = 0; i < bracePos; i++) {
1076
- if (source[i] === '\n')
1077
- lineNo++;
1078
- }
1079
- results.push({ exprStart: bracePos + 1, exprEnd, exprText, lineNo });
1080
- }
1081
- return results;
1082
- }
1083
- export function findForLoopVars(source) {
1084
- const results = [];
1085
- // Match: for (const/let/var ...
1086
- const forRegex = /\bfor\s*\(/g;
1087
- let match;
1088
- while ((match = forRegex.exec(source)) !== null) {
1089
- const afterParen = match.index + match[0].length;
1090
- // Skip whitespace
1091
- let pos = afterParen;
1092
- while (pos < source.length && /\s/.test(source[pos]))
1093
- pos++;
1094
- // Expect const/let/var
1095
- const declMatch = source.slice(pos).match(/^(const|let|var)\s+/);
1096
- if (!declMatch)
1097
- continue;
1098
- pos += declMatch[0].length;
1099
- // Now we have the variable pattern — could be identifier, {destructure}, or [destructure]
1100
- const varNames = [];
1101
- const patternStart = pos;
1102
- if (source[pos] === '{' || source[pos] === '[') {
1103
- // Destructured: find matching brace/bracket
1104
- const open = source[pos];
1105
- const close = open === '{' ? '}' : ']';
1106
- let depth = 1;
1107
- let end = pos + 1;
1108
- while (end < source.length && depth > 0) {
1109
- if (source[end] === open)
1110
- depth++;
1111
- else if (source[end] === close)
1112
- depth--;
1113
- end++;
1114
- }
1115
- const pattern = source.slice(pos, end);
1116
- const names = extractDestructuredNames(pattern);
1117
- varNames.push(...names);
1118
- pos = end;
1119
- }
1120
- else {
1121
- // Simple identifier
1122
- const idMatch = source.slice(pos).match(/^([a-zA-Z_$][a-zA-Z0-9_$]*)/);
1123
- if (!idMatch)
1124
- continue;
1125
- varNames.push(idMatch[1]);
1126
- pos += idMatch[0].length;
1127
- }
1128
- if (varNames.length === 0)
1129
- continue;
1130
- // Skip trickle internals
1131
- if (varNames.every(n => n.startsWith('__trickle') || n === '_a' || n === '_b' || n === '_c'))
1132
- continue;
1133
- // Now find the opening `{` of the loop body
1134
- // Skip everything until the `)` that closes the for(...)
1135
- let parenDepth = 1; // We're inside the for(
1136
- while (pos < source.length && parenDepth > 0) {
1137
- const ch = source[pos];
1138
- if (ch === '(')
1139
- parenDepth++;
1140
- else if (ch === ')')
1141
- parenDepth--;
1142
- else if (ch === '"' || ch === "'" || ch === '`') {
1143
- const q = ch;
1144
- pos++;
1145
- while (pos < source.length && source[pos] !== q) {
1146
- if (source[pos] === '\\')
1147
- pos++;
1148
- pos++;
1149
- }
1150
- }
1151
- pos++;
1152
- }
1153
- // Now find the `{` after the closing `)`
1154
- while (pos < source.length && /\s/.test(source[pos]))
1155
- pos++;
1156
- if (pos >= source.length || source[pos] !== '{')
1157
- continue;
1158
- const bodyBrace = pos;
1159
- // Calculate line number
1160
- let lineNo = 1;
1161
- for (let i = 0; i < match.index; i++) {
1162
- if (source[i] === '\n')
1163
- lineNo++;
1164
- }
1165
- results.push({ bodyStart: bodyBrace + 1, varNames: varNames.filter(n => !n.startsWith('__trickle')), lineNo });
1166
- }
1167
- return results;
1168
- }
1169
- /**
1170
- * Find function parameter names and return insertions for tracing at the start
1171
- * of function bodies. Traces the runtime values of all parameters.
1172
- * Handles: function declarations, arrow functions, method definitions.
1173
- * Skips: React components (already tracked via __trickle_rc with props).
1174
- */
1175
- export function findFunctionParams(source, isReactFile) {
1176
- const results = [];
1177
- // Match function declarations: function name(params) {
1178
- const funcDeclRegex = /\b(?:async\s+)?function\s+([a-zA-Z_$][a-zA-Z0-9_$]*)\s*\(/g;
1179
- let match;
1180
- while ((match = funcDeclRegex.exec(source)) !== null) {
1181
- const name = match[1];
1182
- if (name === 'require' || name === 'exports' || name === 'module')
1183
- continue;
1184
- if (name.startsWith('__trickle'))
1185
- continue;
1186
- // Skip React components (uppercase) in React files — already tracked
1187
- if (isReactFile && /^[A-Z]/.test(name))
1188
- continue;
1189
- const afterParen = match.index + match[0].length;
1190
- const bodyBrace = findFunctionBodyBrace(source, afterParen);
1191
- if (bodyBrace === -1)
1192
- continue;
1193
- // Extract parameter names from between ( and )
1194
- const paramStr = source.slice(afterParen, bodyBrace);
1195
- const closeParen = paramStr.indexOf(')');
1196
- if (closeParen === -1)
1197
- continue;
1198
- const rawParams = paramStr.slice(0, closeParen).trim();
1199
- const paramNames = extractParamNames(rawParams);
1200
- if (paramNames.length === 0)
1201
- continue;
1202
- let lineNo = 1;
1203
- for (let i = 0; i < match.index; i++) {
1204
- if (source[i] === '\n')
1205
- lineNo++;
1206
- }
1207
- results.push({ bodyStart: bodyBrace + 1, paramNames, lineNo });
1208
- }
1209
- // Match arrow functions: const name = (params) => {
1210
- const arrowFuncRegex = /(?:const|let|var)\s+([a-zA-Z_$][a-zA-Z0-9_$]*)(?:\s*:\s*[^=]+?)?\s*=\s*(?:async\s+)?\(([^)]*)\)\s*(?::\s*[^=]+?)?\s*=>\s*\{/g;
1211
- while ((match = arrowFuncRegex.exec(source)) !== null) {
1212
- const name = match[1];
1213
- if (name.startsWith('__trickle'))
1214
- continue;
1215
- // Skip React components in React files
1216
- if (isReactFile && /^[A-Z]/.test(name))
1217
- continue;
1218
- const rawParams = match[2].trim();
1219
- const paramNames = extractParamNames(rawParams);
1220
- if (paramNames.length === 0)
1221
- continue;
1222
- // Find the { position
1223
- const bracePos = match.index + match[0].length - 1;
1224
- let lineNo = 1;
1225
- for (let i = 0; i < match.index; i++) {
1226
- if (source[i] === '\n')
1227
- lineNo++;
1228
- }
1229
- results.push({ bodyStart: bracePos + 1, paramNames, lineNo });
1230
- }
1231
- return results;
1232
- }
1233
- /**
1234
- * Extract parameter names from a function parameter string.
1235
- * Handles: simple names, destructured { a, b }, defaults (a = 1), rest (...args).
1236
- * Skips type annotations.
1237
- */
1238
- function extractParamNames(rawParams) {
1239
- if (!rawParams)
1240
- return [];
1241
- const names = [];
1242
- // Split by commas at depth 0
1243
- const parts = [];
1244
- let depth = 0;
1245
- let current = '';
1246
- for (const ch of rawParams) {
1247
- if (ch === '{' || ch === '[' || ch === '(' || ch === '<')
1248
- depth++;
1249
- else if (ch === '}' || ch === ']' || ch === ')' || ch === '>')
1250
- depth--;
1251
- else if (ch === ',' && depth === 0) {
1252
- parts.push(current.trim());
1253
- current = '';
1254
- continue;
1255
- }
1256
- current += ch;
1257
- }
1258
- if (current.trim())
1259
- parts.push(current.trim());
1260
- for (const part of parts) {
1261
- const trimmed = part.trim();
1262
- if (!trimmed)
1263
- continue;
1264
- // Destructured params — extract individual names
1265
- if (trimmed.startsWith('{')) {
1266
- const closeBrace = trimmed.indexOf('}');
1267
- if (closeBrace !== -1) {
1268
- const destructNames = extractDestructuredNames(trimmed.slice(0, closeBrace + 1));
1269
- names.push(...destructNames);
1270
- }
1271
- continue;
1272
- }
1273
- if (trimmed.startsWith('[')) {
1274
- const closeBracket = trimmed.indexOf(']');
1275
- if (closeBracket !== -1) {
1276
- const destructNames = extractDestructuredNames(trimmed.slice(0, closeBracket + 1));
1277
- names.push(...destructNames);
1278
- }
1279
- continue;
1280
- }
1281
- // Rest parameter: ...args
1282
- if (trimmed.startsWith('...')) {
1283
- const restName = trimmed.slice(3).split(/[\s:=]/)[0].trim();
1284
- if (/^[a-zA-Z_$][a-zA-Z0-9_$]*$/.test(restName)) {
1285
- names.push(restName);
1286
- }
1287
- continue;
1288
- }
1289
- // Simple param: name or name: Type or name = default
1290
- const paramName = trimmed.split(/[\s:=]/)[0].trim();
1291
- if (/^[a-zA-Z_$][a-zA-Z0-9_$]*$/.test(paramName)) {
1292
- names.push(paramName);
1293
- }
1294
- }
1295
- return names;
1296
- }
1297
- /**
1298
- * Transform ESM source code to wrap function declarations and trace variables.
1299
- *
1300
- * Prepends imports of the wrap/trace helpers, then inserts wrapper calls after
1301
- * each function declaration body and trace calls after variable declarations.
1302
- */
1303
- /**
1304
- * Find the original line number for a simple variable declaration.
1305
- * Searches the original source lines for `const/let/var <varName>` near the expected position.
1306
- * Vite transforms typically remove lines (types, imports), so the original line is usually
1307
- * >= the transformed line. We search forward-biased (up to +80) but also a bit backward (-10).
1308
- */
1309
- function findOriginalLine(origLines, varName, transformedLine) {
1310
- const pattern = new RegExp(`\\b(const|let|var)\\s+${escapeRegexStr(varName)}\\b`);
1311
- // Search: first try exact, then expand forward (more likely) and a bit backward
1312
- for (let delta = 0; delta <= 80; delta++) {
1313
- // Forward first (original line is usually after transformed line due to removed TS types)
1314
- const fwd = transformedLine - 1 + delta;
1315
- if (fwd >= 0 && fwd < origLines.length && pattern.test(origLines[fwd])) {
1316
- return fwd + 1;
1317
- }
1318
- // Also check backward (small range)
1319
- if (delta > 0 && delta <= 10) {
1320
- const bwd = transformedLine - 1 - delta;
1321
- if (bwd >= 0 && bwd < origLines.length && pattern.test(origLines[bwd])) {
1322
- return bwd + 1;
1323
- }
1324
- }
1325
- }
1326
- return -1;
1327
- }
1328
- /**
1329
- * Find the original line number for a destructured declaration.
1330
- * Searches for const/let/var { or [ patterns containing at least one of the variable names.
1331
- */
1332
- function findOriginalLineDestructured(origLines, varNames, transformedLine) {
1333
- // Match names as actual bindings (not renamed property keys).
1334
- // In `{ data: customer }`, 'data' is a key (followed by ':'), 'customer' is the binding.
1335
- // In `{ data, error }`, 'data' is a binding (followed by ',' or '}').
1336
- // We check: name followed by comma, }, ], =, whitespace, or end — NOT followed by ':' (rename).
1337
- const namePatterns = varNames.map(n => new RegExp(`\\b${escapeRegexStr(n)}\\b(?!\\s*:)`));
1338
- for (let delta = 0; delta <= 80; delta++) {
1339
- const fwd = transformedLine - 1 + delta;
1340
- if (fwd >= 0 && fwd < origLines.length) {
1341
- const line = origLines[fwd];
1342
- if (/\b(const|let|var)\s+[\[{]/.test(line) && namePatterns.some(p => p.test(line))) {
1343
- return fwd + 1;
1344
- }
1345
- }
1346
- if (delta > 0 && delta <= 10) {
1347
- const bwd = transformedLine - 1 - delta;
1348
- if (bwd >= 0 && bwd < origLines.length) {
1349
- const line = origLines[bwd];
1350
- if (/\b(const|let|var)\s+[\[{]/.test(line) && namePatterns.some(p => p.test(line))) {
1351
- return bwd + 1;
1352
- }
1353
- }
1354
- }
1355
- }
1356
- return -1;
1357
- }
1358
- function escapeRegexStr(str) {
1359
- return str.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
1360
- }
1361
- export function transformEsmSource(source, filename, moduleName, backendUrl, debug, traceVars, originalSource, isSSR,
1362
- /** URL for fetch-based browser transport (Next.js client). When set and isSSR=false, uses fetch() instead of import.meta.hot */
1363
- ingestUrl) {
1364
- // Detect React files for component render tracking
1365
- const isReactFile = /\.(tsx|jsx)$/.test(filename);
1366
- // Match top-level and nested function declarations (including async, export, export default)
1367
- const funcRegex = /^[ \t]*(?:export\s+(?:default\s+)?)?(?:async\s+)?function\s+([a-zA-Z_$][a-zA-Z0-9_$]*)\s*\(/gm;
1368
- const funcInsertions = [];
1369
- // Body insertions: insert at start of function body (for React render tracking)
1370
- // propsExpr: JS expression to evaluate as the props object at render time
1371
- const bodyInsertions = [];
1372
- let match;
1373
- while ((match = funcRegex.exec(source)) !== null) {
1374
- const name = match[1];
1375
- if (name === 'require' || name === 'exports' || name === 'module')
1376
- continue;
1377
- const afterMatch = match.index + match[0].length;
1378
- // Use findFunctionBodyBrace to correctly skip destructured params like ({ a, b }) =>
1379
- const openBrace = findFunctionBodyBrace(source, afterMatch);
1380
- if (openBrace === -1)
1381
- continue;
1382
- // Extract parameter names (between the opening ( and the body {)
1383
- const paramStr = source.slice(afterMatch, openBrace).replace(/[()]/g, '').trim();
1384
- const paramNames = paramStr
1385
- ? paramStr.split(',').map(p => {
1386
- const trimmed = p.trim().split('=')[0].trim().split(':')[0].trim();
1387
- if (trimmed.startsWith('{') || trimmed.startsWith('[') || trimmed.startsWith('...'))
1388
- return '';
1389
- return trimmed;
1390
- }).filter(Boolean)
1391
- : [];
1392
- const closeBrace = findClosingBrace(source, openBrace);
1393
- if (closeBrace === -1)
1394
- continue;
1395
- funcInsertions.push({ position: closeBrace + 1, name, paramNames });
1396
- // React component render tracking: uppercase function name in .tsx/.jsx
1397
- // function declarations have `arguments`, so arguments[0] is the raw props object
1398
- if (isReactFile && /^[A-Z]/.test(name)) {
1399
- let lineNo = 1;
1400
- for (let i = 0; i < match.index; i++) {
1401
- if (source[i] === '\n')
1402
- lineNo++;
1403
- }
1404
- bodyInsertions.push({ position: openBrace + 1, name, lineNo, propsExpr: 'arguments[0]' });
1405
- }
1406
- }
1407
- // Also match arrow functions assigned to const/let/var
1408
- // Handles: const X = () => {}, const X: React.FC = () => {}, const X: React.FC<Props> = ({ a }) => {}
1409
- // Also handles concise bodies: const X = (props) => (<div/>)
1410
- const arrowRegex = /^[ \t]*(?:export\s+)?(?:const|let|var)\s+([a-zA-Z_$][a-zA-Z0-9_$]*)(?:\s*:\s*[^=]+?)?\s*=\s*(?:async\s+)?(?:\([^)]*\)|[a-zA-Z_$][a-zA-Z0-9_$]*)\s*(?::\s*[^=]+?)?\s*=>\s*(?:\{|\()/gm;
1411
- // Concise body insertions: for `=> (expr)`, wrap with block body for render tracking
1412
- const conciseBodyInsertions = [];
1413
- while ((match = arrowRegex.exec(source)) !== null) {
1414
- const name = match[1];
1415
- const bodyStartPos = match.index + match[0].length - 1;
1416
- const isConcise = source[bodyStartPos] === '(';
1417
- const arrowStr = match[0];
1418
- const arrowParamMatch = arrowStr.match(/=\s*(?:async\s+)?(?:\(([^)]*)\)|([a-zA-Z_$][a-zA-Z0-9_$]*))\s*(?::\s*[^=]+?)?\s*=>/);
1419
- let paramNames = [];
1420
- if (arrowParamMatch) {
1421
- const paramStr = (arrowParamMatch[1] || arrowParamMatch[2] || '').trim();
1422
- if (paramStr) {
1423
- paramNames = paramStr.split(',').map(p => {
1424
- const trimmed = p.trim().split('=')[0].trim().split(':')[0].trim();
1425
- if (trimmed.startsWith('{') || trimmed.startsWith('[') || trimmed.startsWith('...'))
1426
- return '';
1427
- return trimmed;
1428
- }).filter(Boolean);
1429
- }
1430
- }
1431
- // Helper to build propsExpr from arrowParamMatch
1432
- const buildPropsExpr = () => {
1433
- if (!arrowParamMatch)
1434
- return 'undefined';
1435
- const rawParams = (arrowParamMatch[1] || '').trim();
1436
- if (!rawParams)
1437
- return 'undefined';
1438
- if (rawParams.startsWith('{')) {
1439
- let depth2 = 0, endBrace = -1;
1440
- for (let i = 0; i < rawParams.length; i++) {
1441
- if (rawParams[i] === '{')
1442
- depth2++;
1443
- else if (rawParams[i] === '}') {
1444
- depth2--;
1445
- if (depth2 === 0) {
1446
- endBrace = i;
1447
- break;
1448
- }
1449
- }
1450
- }
1451
- const destructPattern = endBrace !== -1 ? rawParams.slice(0, endBrace + 1) : rawParams;
1452
- const fields = extractDestructuredNames(destructPattern);
1453
- return fields.length > 0 ? `{ ${fields.join(', ')} }` : 'undefined';
1454
- }
1455
- else if (arrowParamMatch[2]) {
1456
- return arrowParamMatch[2];
1457
- }
1458
- else if (paramNames.length === 1) {
1459
- return paramNames[0];
1460
- }
1461
- return 'undefined';
1462
- };
1463
- if (isConcise) {
1464
- // Concise body: `const X = (props) => (<div/>)` — no block body
1465
- // Only add render tracking for React components (uppercase names in .tsx/.jsx)
1466
- if (isReactFile && /^[A-Z]/.test(name)) {
1467
- const closeParen = findMatchingParen(source, bodyStartPos);
1468
- if (closeParen === -1)
1469
- continue;
1470
- let lineNo = 1;
1471
- for (let i = 0; i < match.index; i++) {
1472
- if (source[i] === '\n')
1473
- lineNo++;
1474
- }
1475
- conciseBodyInsertions.push({ beforeParen: bodyStartPos, afterCloseParen: closeParen + 1, name, lineNo, propsExpr: buildPropsExpr() });
1476
- }
1477
- }
1478
- else {
1479
- // Block body: `const X = (props) => { ... }`
1480
- const openBrace = bodyStartPos;
1481
- const closeBrace = findClosingBrace(source, openBrace);
1482
- if (closeBrace === -1)
1483
- continue;
1484
- funcInsertions.push({ position: closeBrace + 1, name, paramNames });
1485
- // React component render tracking: uppercase arrow function in .tsx/.jsx
1486
- if (isReactFile && /^[A-Z]/.test(name)) {
1487
- let lineNo = 1;
1488
- for (let i = 0; i < match.index; i++) {
1489
- if (source[i] === '\n')
1490
- lineNo++;
1491
- }
1492
- bodyInsertions.push({ position: openBrace + 1, name, lineNo, propsExpr: buildPropsExpr() });
1493
- }
1494
- }
1495
- }
1496
- // Match React.memo() and React.forwardRef() wrapped components
1497
- // Pattern: const Name = (React.)?memo( or const Name = (React.)?forwardRef<T,P>(
1498
- // Then scan forward to find the inner arrow => { body
1499
- if (isReactFile) {
1500
- const memoRefRegex = /^[ \t]*(?:export\s+)?(?:const|let|var)\s+([A-Z][a-zA-Z0-9_$]*)(?:\s*:\s*[^=]+?)?\s*=\s*(?:React\.)?(?:memo|forwardRef)\s*(?:<[^(]*>)?\s*\(/gm;
1501
- let memoMatch;
1502
- while ((memoMatch = memoRefRegex.exec(source)) !== null) {
1503
- const name = memoMatch[1];
1504
- // Position after the opening `(` of memo/forwardRef call
1505
- const afterMemoOpen = memoMatch.index + memoMatch[0].length;
1506
- // Scan forward to find `=> {` — the arrow body of the inner function.
1507
- // We need to skip over the inner function's parameter list (which may contain nested parens).
1508
- // Strategy: find the next `=>` that is followed by optional whitespace and `{`.
1509
- let pos = afterMemoOpen;
1510
- let arrowPos = -1;
1511
- let parenDepth = 0;
1512
- while (pos < source.length - 1) {
1513
- const ch = source[pos];
1514
- if (ch === '(')
1515
- parenDepth++;
1516
- else if (ch === ')')
1517
- parenDepth--;
1518
- else if (ch === '=' && source[pos + 1] === '>' && parenDepth <= 0) {
1519
- arrowPos = pos;
1520
- break;
1521
- }
1522
- pos++;
1523
- }
1524
- if (arrowPos === -1)
1525
- continue;
1526
- // Skip `=>` and whitespace to find `{`
1527
- let bracePos = arrowPos + 2;
1528
- while (bracePos < source.length && /[\s]/.test(source[bracePos]))
1529
- bracePos++;
1530
- if (source[bracePos] !== '{')
1531
- continue;
1532
- const openBrace = bracePos;
1533
- const closeBrace = findClosingBrace(source, openBrace);
1534
- if (closeBrace === -1)
1535
- continue;
1536
- // Extract the param list: everything between memo( and arrowPos
1537
- const innerParamStr = source.slice(afterMemoOpen, arrowPos).trim();
1538
- // innerParamStr is like `({ item, onSelect })` or `(props, ref)` or `props`
1539
- let propsExpr = 'undefined';
1540
- if (innerParamStr.startsWith('(')) {
1541
- // Peel outer parens
1542
- const inner = innerParamStr.slice(1, innerParamStr.lastIndexOf(')')).trim();
1543
- if (inner.startsWith('{')) {
1544
- // Find the matching `}` of the destructuring pattern, ignoring any type annotation after it
1545
- let depth3 = 0, destructEnd = -1;
1546
- for (let i = 0; i < inner.length; i++) {
1547
- if (inner[i] === '{')
1548
- depth3++;
1549
- else if (inner[i] === '}') {
1550
- depth3--;
1551
- if (depth3 === 0) {
1552
- destructEnd = i;
1553
- break;
1554
- }
1555
- }
1556
- }
1557
- const destructPart = destructEnd !== -1 ? inner.slice(0, destructEnd + 1) : inner;
1558
- const fields = extractDestructuredNames(destructPart);
1559
- if (fields.length > 0)
1560
- propsExpr = `{ ${fields.join(', ')} }`;
1561
- }
1562
- else {
1563
- const firstParam = inner.split(',')[0].trim().split(':')[0].trim();
1564
- if (firstParam)
1565
- propsExpr = firstParam;
1566
- }
1567
- }
1568
- else if (innerParamStr && /^[a-zA-Z_$]/.test(innerParamStr)) {
1569
- propsExpr = innerParamStr.split(/[\s,:(]/)[0];
1570
- }
1571
- let lineNo = 1;
1572
- for (let i = 0; i < memoMatch.index; i++) {
1573
- if (source[i] === '\n')
1574
- lineNo++;
1575
- }
1576
- bodyInsertions.push({ position: openBrace + 1, name, lineNo, propsExpr });
1577
- }
1578
- }
1579
- const hookInsertions = [];
1580
- if (isReactFile) {
1581
- // Match useEffect(, useMemo(, useCallback( — also handles React.useEffect(, etc.
1582
- const hookCallRegex = /\b(useEffect|useMemo|useCallback)\s*\(/g;
1583
- let hookMatch;
1584
- while ((hookMatch = hookCallRegex.exec(source)) !== null) {
1585
- const hookName = hookMatch[1];
1586
- const afterParen = hookMatch.index + hookMatch[0].length;
1587
- // Skip past optional 'async '
1588
- let pos = afterParen;
1589
- while (pos < source.length && (source[pos] === ' ' || source[pos] === '\t' || source[pos] === '\n'))
1590
- pos++;
1591
- if (source.slice(pos, pos + 6) === 'async ') {
1592
- pos += 6;
1593
- while (pos < source.length && (source[pos] === ' ' || source[pos] === '\t'))
1594
- pos++;
1595
- }
1596
- // Expect a callback: arrow fn `(` or `identifier =>` or `function`
1597
- if (source[pos] !== '(' && !/^[a-zA-Z_$]/.test(source[pos]) && source.slice(pos, pos + 8) !== 'function')
1598
- continue;
1599
- // Find the opening `{` of the callback body depending on callback form:
1600
- // 1. Arrow with parens: (x, y) => { — call findFunctionBodyBrace from inside the (
1601
- // 2. Named/anon function: function() { — find the ( first
1602
- // 3. Single identifier: props => { — skip identifier, find =>, find {
1603
- let callbackBodyBrace = -1;
1604
- if (source[pos] === '(') {
1605
- // Arrow function with param list: () => { ... } or (x) => { ... }
1606
- callbackBodyBrace = findFunctionBodyBrace(source, pos + 1);
1607
- }
1608
- else if (source.slice(pos, pos + 8) === 'function') {
1609
- // function() {} or function name() {}
1610
- let funcPos = pos + 8;
1611
- while (funcPos < source.length && /\s/.test(source[funcPos]))
1612
- funcPos++;
1613
- if (/[a-zA-Z_$]/.test(source[funcPos])) {
1614
- while (funcPos < source.length && /[a-zA-Z0-9_$]/.test(source[funcPos]))
1615
- funcPos++;
1616
- }
1617
- while (funcPos < source.length && source[funcPos] !== '(')
1618
- funcPos++;
1619
- if (funcPos < source.length) {
1620
- callbackBodyBrace = findFunctionBodyBrace(source, funcPos + 1);
1621
- }
1622
- }
1623
- else {
1624
- // Single identifier param: props => { ... }
1625
- let idEnd = pos;
1626
- while (idEnd < source.length && /[a-zA-Z0-9_$]/.test(source[idEnd]))
1627
- idEnd++;
1628
- let arrowPos = idEnd;
1629
- while (arrowPos < source.length && (source[arrowPos] === ' ' || source[arrowPos] === '\t'))
1630
- arrowPos++;
1631
- if (source.slice(arrowPos, arrowPos + 2) === '=>') {
1632
- arrowPos += 2;
1633
- while (arrowPos < source.length && (source[arrowPos] === ' ' || source[arrowPos] === '\t' || source[arrowPos] === '\n'))
1634
- arrowPos++;
1635
- if (source[arrowPos] === '{')
1636
- callbackBodyBrace = arrowPos;
1637
- }
1638
- }
1639
- if (callbackBodyBrace === -1)
1640
- continue;
1641
- // Verify nothing suspicious between pos and the `{` (no semicolons, no other hook calls)
1642
- const between = source.slice(pos, callbackBodyBrace);
1643
- if (between.includes(';') || /\buseEffect\b|\buseMemo\b|\buseCallback\b/.test(between))
1644
- continue;
1645
- const closeBrace = findClosingBrace(source, callbackBodyBrace);
1646
- if (closeBrace === -1)
1647
- continue;
1648
- let lineNo = 1;
1649
- for (let i = 0; i < hookMatch.index; i++) {
1650
- if (source[i] === '\n')
1651
- lineNo++;
1652
- }
1653
- hookInsertions.push({ wrapStart: afterParen, wrapEnd: closeBrace + 1, hookName, lineNo });
1654
- }
1655
- }
1656
- const stateInsertions = [];
1657
- if (isReactFile) {
1658
- const useStateRegex = /const\s+\[([a-zA-Z_$][a-zA-Z0-9_$]*)\s*,\s*([a-zA-Z_$][a-zA-Z0-9_$]*)\s*\]\s*=\s*(?:React\.)?useState\s*(?:<[^(]*>)?\s*\(/gm;
1659
- let sm;
1660
- while ((sm = useStateRegex.exec(source)) !== null) {
1661
- const stateName = sm[1];
1662
- const setterName = sm[2];
1663
- // Find the position of setterName within the match (after the comma)
1664
- const matchStr = sm[0];
1665
- const commaIdx = matchStr.indexOf(',');
1666
- const setterInMatch = matchStr.indexOf(setterName, commaIdx);
1667
- if (setterInMatch === -1)
1668
- continue;
1669
- const renamePos = sm.index + setterInMatch;
1670
- // Skip the useState(...) argument list to find the end of the statement
1671
- let pos = sm.index + sm[0].length;
1672
- let depth = 1;
1673
- while (pos < source.length && depth > 0) {
1674
- const ch = source[pos];
1675
- if (ch === '(')
1676
- depth++;
1677
- else if (ch === ')')
1678
- depth--;
1679
- else if (ch === '"' || ch === "'" || ch === '`') {
1680
- const q = ch;
1681
- pos++;
1682
- while (pos < source.length && source[pos] !== q) {
1683
- if (source[pos] === '\\')
1684
- pos++;
1685
- pos++;
1686
- }
1687
- }
1688
- pos++;
1689
- }
1690
- // Skip to end of line (past semicolon or newline)
1691
- while (pos < source.length && source[pos] !== ';' && source[pos] !== '\n')
1692
- pos++;
1693
- const afterLine = pos + 1;
1694
- let lineNo = 1;
1695
- for (let i = 0; i < sm.index; i++) {
1696
- if (source[i] === '\n')
1697
- lineNo++;
1698
- }
1699
- stateInsertions.push({ renamePos, afterLine, stateName, setterName, lineNo });
1700
- }
1701
- }
1702
- // Find import declarations for tracing (trace imported bindings after import statement)
1703
- const importInsertions = traceVars ? findImportDeclarations(source) : [];
1704
- // Find variable declarations for tracing
1705
- const varInsertions = traceVars ? findVarDeclarations(source) : [];
1706
- // Find destructured variable declarations for tracing
1707
- const destructInsertions = traceVars ? findDestructuredDeclarations(source) : [];
1708
- // Find variable reassignments for tracing
1709
- const reassignInsertions = traceVars ? findReassignments(source) : [];
1710
- // Find for-loop variable declarations for tracing
1711
- const forLoopInsertions = traceVars ? findForLoopVars(source) : [];
1712
- // Find catch clause variables for tracing
1713
- const catchInsertions = traceVars ? findCatchVars(source) : [];
1714
- // Find function parameter names for tracing
1715
- const funcParamInsertions = traceVars ? findFunctionParams(source, isReactFile) : [];
1716
- // Find JSX text expressions for tracing (React files only).
1717
- // Skip if JSX has already been compiled to _jsxDEV/jsx/jsxs calls (e.g. by Vite's React plugin).
1718
- // In that case, the `{` characters in the source are plain JS (function bodies, object literals)
1719
- // and findJsxExpressions would corrupt them by injecting __trickle_tv() calls.
1720
- const jsxAlreadyCompiled = /\b_?jsxDEV\b|\bjsxs?\s*\(/.test(source);
1721
- const jsxExprInsertions = (traceVars && isReactFile && !jsxAlreadyCompiled) ? findJsxExpressions(source) : [];
1722
- if (funcInsertions.length === 0 && importInsertions.length === 0 && varInsertions.length === 0 && destructInsertions.length === 0 && reassignInsertions.length === 0 && forLoopInsertions.length === 0 && catchInsertions.length === 0 && funcParamInsertions.length === 0 && jsxExprInsertions.length === 0 && bodyInsertions.length === 0 && hookInsertions.length === 0 && stateInsertions.length === 0 && conciseBodyInsertions.length === 0)
1723
- return source;
1724
- // Fix line numbers: Vite transforms (TypeScript stripping) may change line numbers.
1725
- // Map transformed line numbers to original source line numbers.
1726
- if (originalSource && originalSource !== source) {
1727
- const origLines = originalSource.split('\n');
1728
- // For each variable insertion, find the declaration in the original source
1729
- for (const vi of varInsertions) {
1730
- const origLine = findOriginalLine(origLines, vi.varName, vi.lineNo);
1731
- if (origLine !== -1)
1732
- vi.lineNo = origLine;
1733
- }
1734
- for (const di of destructInsertions) {
1735
- // Use the first variable name to locate the line
1736
- if (di.varNames.length > 0) {
1737
- const origLine = findOriginalLineDestructured(origLines, di.varNames, di.lineNo);
1738
- if (origLine !== -1)
1739
- di.lineNo = origLine;
1740
- }
1741
- }
1742
- // Fix reassignment line numbers
1743
- for (const ri of reassignInsertions) {
1744
- const origLine = findOriginalLine(origLines, ri.varName, ri.lineNo);
1745
- if (origLine !== -1)
1746
- ri.lineNo = origLine;
1747
- }
1748
- // Fix for-loop var line numbers
1749
- for (const fi of forLoopInsertions) {
1750
- if (fi.varNames.length > 0) {
1751
- // Search for 'for' keyword near the expected line
1752
- const pattern = /\bfor\s*\(/;
1753
- for (let delta = 0; delta <= 80; delta++) {
1754
- const fwd = fi.lineNo - 1 + delta;
1755
- if (fwd >= 0 && fwd < origLines.length && pattern.test(origLines[fwd])) {
1756
- fi.lineNo = fwd + 1;
1757
- break;
1758
- }
1759
- if (delta > 0 && delta <= 10) {
1760
- const bwd = fi.lineNo - 1 - delta;
1761
- if (bwd >= 0 && bwd < origLines.length && pattern.test(origLines[bwd])) {
1762
- fi.lineNo = bwd + 1;
1763
- break;
1764
- }
1765
- }
1766
- }
1767
- }
1768
- }
1769
- // Fix function param line numbers
1770
- for (const fp of funcParamInsertions) {
1771
- if (fp.paramNames.length > 0) {
1772
- const pattern = /\bfunction\s+\w+\s*\(|=>\s*\{/;
1773
- for (let delta = 0; delta <= 80; delta++) {
1774
- const fwd = fp.lineNo - 1 + delta;
1775
- if (fwd >= 0 && fwd < origLines.length && pattern.test(origLines[fwd])) {
1776
- fp.lineNo = fwd + 1;
1777
- break;
1778
- }
1779
- if (delta > 0 && delta <= 10) {
1780
- const bwd = fp.lineNo - 1 - delta;
1781
- if (bwd >= 0 && bwd < origLines.length && pattern.test(origLines[bwd])) {
1782
- fp.lineNo = bwd + 1;
1783
- break;
1784
- }
1785
- }
1786
- }
1787
- }
1788
- }
1789
- }
1790
- // Build prefix — ALL imports first (ESM requires imports before any statements)
1791
- const needsTracing = importInsertions.length > 0 || varInsertions.length > 0 || destructInsertions.length > 0 || reassignInsertions.length > 0 || forLoopInsertions.length > 0 || catchInsertions.length > 0 || funcParamInsertions.length > 0 || jsxExprInsertions.length > 0 || bodyInsertions.length > 0 || hookInsertions.length > 0 || stateInsertions.length > 0 || conciseBodyInsertions.length > 0;
1792
- const importLines = [];
1793
- if (isSSR) {
1794
- // SSR/Node.js — import trickle-observe for function wrapping + file system for writing
1795
- importLines.push(`import { wrapFunction as __trickle_wrapFn, configure as __trickle_configure } from 'trickle-observe';`);
1796
- if (needsTracing) {
1797
- importLines.push(`import { mkdirSync as __trickle_mkdirSync, appendFileSync as __trickle_appendFileSync } from 'node:fs';`, `import { join as __trickle_join } from 'node:path';`);
1798
- }
1799
- }
1800
- // Browser mode: no imports needed — variable tracers are self-contained,
1801
- // function wrapping is a no-op, and transport uses import.meta.hot
1802
- const prefixLines = [...importLines];
1803
- if (isSSR) {
1804
- prefixLines.push(`__trickle_configure({ backendUrl: ${JSON.stringify(backendUrl)}, batchIntervalMs: 2000, debug: ${debug}, enabled: true, environment: 'node' });`, `function __trickle_wrap(fn, name, paramNames) {`, ` const opts = {`, ` functionName: name,`, ` module: ${JSON.stringify(moduleName)},`, ` trackArgs: true,`, ` trackReturn: true,`, ` sampleRate: 1,`, ` maxDepth: 5,`, ` environment: 'node',`, ` enabled: true,`, ` };`, ` if (paramNames && paramNames.length) opts.paramNames = paramNames;`, ` return __trickle_wrapFn(fn, opts);`, `}`);
1805
- }
1806
- else {
1807
- // Browser mode: __trickle_wrap is a no-op (function wrapping uses Node.js APIs)
1808
- prefixLines.push(`function __trickle_wrap(fn) { return fn; }`);
1809
- }
1810
- // Add unified __trickle_send() transport — browser uses HMR WebSocket, SSR uses fs
1811
- if (needsTracing) {
1812
- if (isSSR) {
1813
- // SSR/Node.js mode — write directly to file system
1814
- prefixLines.push(`let __trickle_varsFile = null;`, `function __trickle_send(line) {`, ` try {`, ` if (!__trickle_varsFile) {`, ` const dir = process.env.TRICKLE_LOCAL_DIR || __trickle_join(process.cwd(), '.trickle');`, ` try { __trickle_mkdirSync(dir, { recursive: true }); } catch(e) {}`, ` __trickle_varsFile = __trickle_join(dir, 'variables.jsonl');`, ` }`, ` __trickle_appendFileSync(__trickle_varsFile, line + '\\n');`, ` } catch(e) {}`, `}`);
1815
- }
1816
- else if (ingestUrl) {
1817
- // Browser mode with fetch transport (Next.js client)
1818
- prefixLines.push(`const __trickle_sendBuf = [];`, `let __trickle_sendTimer = null;`, `function __trickle_flush() {`, ` if (__trickle_sendBuf.length === 0) return;`, ` const lines = __trickle_sendBuf.join('\\n') + '\\n';`, ` __trickle_sendBuf.length = 0;`, ` try { fetch(${JSON.stringify(ingestUrl)}, { method: 'POST', body: lines, headers: { 'Content-Type': 'text/plain' } }).catch(function(){}); } catch(e) {}`, `}`, `function __trickle_send(line) {`, ` __trickle_sendBuf.push(line);`, ` if (!__trickle_sendTimer) {`, ` __trickle_sendTimer = setTimeout(function() { __trickle_sendTimer = null; __trickle_flush(); }, 300);`, ` }`, `}`);
1819
- }
1820
- else {
1821
- // Browser mode — buffer and send via Vite HMR WebSocket
1822
- prefixLines.push(`const __trickle_sendBuf = [];`, `let __trickle_sendTimer = null;`, `function __trickle_flush() {`, ` if (__trickle_sendBuf.length === 0) return;`, ` const lines = __trickle_sendBuf.join('\\n') + '\\n';`, ` __trickle_sendBuf.length = 0;`, ` try { if (import.meta.hot) import.meta.hot.send('trickle:vars', { lines }); } catch(e) {}`, `}`, `function __trickle_send(line) {`, ` __trickle_sendBuf.push(line);`, ` if (!__trickle_sendTimer) {`, ` __trickle_sendTimer = setTimeout(function() { __trickle_sendTimer = null; __trickle_flush(); }, 300);`, ` }`, `}`);
1823
- }
1824
- }
1825
- // Add variable tracing if needed — inlined to avoid import resolution issues in Vite SSR.
1826
- if (importInsertions.length > 0 || varInsertions.length > 0 || destructInsertions.length > 0 || reassignInsertions.length > 0 || forLoopInsertions.length > 0 || catchInsertions.length > 0 || funcParamInsertions.length > 0 || jsxExprInsertions.length > 0) {
1827
- prefixLines.push(`if (!globalThis.__trickle_var_tracer) {`, ` const _cache = new Map();`, ` const _sampleCount = new Map();`, ` const _MAX_SAMPLES = 5;`, ` function _inferType(v, d) {`, ` if (d <= 0) return { kind: 'primitive', name: 'unknown' };`, ` if (v === null) return { kind: 'primitive', name: 'null' };`, ` if (v === undefined) return { kind: 'primitive', name: 'undefined' };`, ` const t = typeof v;`, ` if (t === 'string' || t === 'number' || t === 'boolean' || t === 'bigint' || t === 'symbol') return { kind: 'primitive', name: t };`, ` if (t === 'function') return { kind: 'function' };`, ` if (Array.isArray(v)) { return v.length === 0 ? { kind: 'array', element: { kind: 'primitive', name: 'unknown' } } : { kind: 'array', element: _inferType(v[0], d-1) }; }`, ` if (t === 'object') {`, ` if (v instanceof Date) return { kind: 'object', properties: { __date: { kind: 'primitive', name: 'string' } } };`, ` if (v instanceof RegExp) return { kind: 'object', properties: { __regexp: { kind: 'primitive', name: 'string' } } };`, ` if (v instanceof Error) return { kind: 'object', properties: { __error: { kind: 'primitive', name: 'string' } } };`, ` if (v instanceof Promise) return { kind: 'promise', resolved: { kind: 'primitive', name: 'unknown' } };`, ` const props = {}; const keys = Object.keys(v).slice(0, 20);`, ` for (const k of keys) { try { props[k] = _inferType(v[k], d-1); } catch(e) { props[k] = { kind: 'primitive', name: 'unknown' }; } }`, ` return { kind: 'object', properties: props };`, ` }`, ` return { kind: 'primitive', name: 'unknown' };`, ` }`, ` function _sanitize(v, d) {`, ` if (d <= 0) return '[truncated]'; if (v === null || v === undefined) return v; const t = typeof v;`, ` if (t === 'string') return v.length > 100 ? v.substring(0, 100) + '...' : v;`, ` if (t === 'number' || t === 'boolean') return v; if (t === 'bigint') return String(v);`, ` if (t === 'function') return '[Function: ' + (v.name || 'anonymous') + ']';`, ` if (Array.isArray(v)) return v.slice(0, 20).map(i => _sanitize(i, d-1));`, ` if (t === 'object') { if (v instanceof Date) return v.toISOString(); if (v instanceof RegExp) return String(v); if (v instanceof Error) return { error: v.message }; if (v instanceof Promise) return '[Promise]';`, ` const r = {}; const keys = Object.keys(v).slice(0, 10); for (const k of keys) { try { r[k] = _sanitize(v[k], d-1); } catch(e) { r[k] = '[unreadable]'; } } return r; }`, ` return String(v);`, ` }`, ` globalThis.__trickle_var_tracer = function(v, n, l, mod, file) {`, ` try {`, ` const type = _inferType(v, 3);`, ` const th = JSON.stringify(type).substring(0, 32);`, ` const sample = _sanitize(v, 2);`, ` const sv = typeof v === 'object' && v !== null ? JSON.stringify(sample).substring(0, 60) : String(v).substring(0, 60);`, ` const ck = file + ':' + l + ':' + n;`, ` const cnt = _sampleCount.get(ck) || 0;`, ` if (cnt >= _MAX_SAMPLES) return;`, ` const prev = _cache.get(ck);`, ` const now = Date.now();`, ` if (prev && prev.sv === sv && now - prev.ts < 5000) return;`, ` _cache.set(ck, { sv: sv, ts: now });`, ` _sampleCount.set(ck, cnt + 1);`, ` __trickle_send(JSON.stringify({ kind: 'variable', varName: n, line: l, module: mod, file: file, type: type, typeHash: th, sample: sample }));`, ` } catch(e) {}`, ` };`, `}`, `function __trickle_tv(v, n, l) { try { globalThis.__trickle_var_tracer(v, n, l, ${JSON.stringify(moduleName)}, ${JSON.stringify(filename)}); } catch(e) {} }`);
1828
- }
1829
- // Add React component render tracker if needed
1830
- if (bodyInsertions.length > 0 || conciseBodyInsertions.length > 0) {
1831
- 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 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;`, ` 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_send(JSON.stringify(rec));`, ` } catch(e) {}`, `}`);
1832
- }
1833
- // Add React hook tracker if needed
1834
- if (hookInsertions.length > 0) {
1835
- prefixLines.push(`if (!globalThis.__trickle_hook_counts) { globalThis.__trickle_hook_counts = new Map(); }`, `function __trickle_hw(hookName, line, cb) {`, ` return function(...args) {`, ` try {`, ` const key = ${JSON.stringify(filename)} + ':' + line + ':' + hookName;`, ` const n = (globalThis.__trickle_hook_counts.get(key) || 0) + 1;`, ` globalThis.__trickle_hook_counts.set(key, n);`, ` __trickle_send(JSON.stringify({ kind: 'react_hook', hookName, file: ${JSON.stringify(filename)}, line, invokeCount: n, timestamp: Date.now() / 1000 }));`, ` } catch(e) {}`, ` return cb(...args);`, ` };`, `}`);
1836
- }
1837
- // Add useState setter tracker if needed
1838
- if (stateInsertions.length > 0) {
1839
- prefixLines.push(`if (!globalThis.__trickle_state_counts) { globalThis.__trickle_state_counts = new Map(); }`, `function __trickle_ss(stateName, line, origSetter) {`, ` return function(newVal) {`, ` try {`, ` const key = ${JSON.stringify(filename)} + ':' + line + ':' + stateName;`, ` const n = (globalThis.__trickle_state_counts.get(key) || 0) + 1;`, ` globalThis.__trickle_state_counts.set(key, n);`, ` const t = typeof newVal;`, ` let sample;`, ` if (t === 'function') sample = '[fn updater]';`, ` else if (t === 'string') sample = newVal.length > 40 ? newVal.slice(0,40)+'...' : newVal;`, ` else if (t === 'number' || t === 'boolean') sample = newVal;`, ` else if (newVal === null || newVal === undefined) sample = newVal;`, ` else if (Array.isArray(newVal)) sample = '[arr:'+newVal.length+']';`, ` else sample = '[object]';`, ` __trickle_send(JSON.stringify({ kind: 'react_state', file: ${JSON.stringify(filename)}, line, stateName, updateCount: n, value: sample, timestamp: Date.now()/1000 }));`, ` } catch(e) {}`, ` return origSetter(newVal);`, ` };`, `}`);
1840
- }
1841
- prefixLines.push('');
1842
- const prefix = prefixLines.join('\n');
1843
- const allInsertions = [];
1844
- for (const { position, name, paramNames } of funcInsertions) {
1845
- const paramNamesArg = paramNames.length > 0 ? JSON.stringify(paramNames) : 'null';
1846
- allInsertions.push({
1847
- position,
1848
- code: `\ntry{${name}=__trickle_wrap(${name},'${name}',${paramNamesArg})}catch(__e){}\n`,
1849
- });
1850
- }
1851
- // Import insertions: trace imported bindings after the import statement
1852
- for (const { lineEnd, varNames, lineNo } of importInsertions) {
1853
- const calls = varNames.map(n => `__trickle_tv(${n},${JSON.stringify(n)},${lineNo})`).join(';');
1854
- allInsertions.push({
1855
- position: lineEnd,
1856
- code: `\n;try{${calls}}catch(__e){}\n`,
1857
- });
1858
- }
1859
- for (const { lineEnd, varName, lineNo } of varInsertions) {
1860
- allInsertions.push({
1861
- position: lineEnd,
1862
- code: `\n;try{__trickle_tv(${varName},${JSON.stringify(varName)},${lineNo})}catch(__e){}\n`,
1863
- });
1864
- }
1865
- for (const { lineEnd, varNames, lineNo } of destructInsertions) {
1866
- const calls = varNames.map(n => `__trickle_tv(${n},${JSON.stringify(n)},${lineNo})`).join(';');
1867
- allInsertions.push({
1868
- position: lineEnd,
1869
- code: `\n;try{${calls}}catch(__e){}\n`,
1870
- });
1871
- }
1872
- // Reassignment insertions: trace after the reassignment statement
1873
- for (const { lineEnd, varName, lineNo } of reassignInsertions) {
1874
- allInsertions.push({
1875
- position: lineEnd,
1876
- code: `\n;try{__trickle_tv(${varName},${JSON.stringify(varName)},${lineNo})}catch(__e){}\n`,
1877
- });
1878
- }
1879
- // Catch clause insertions: insert trace at start of catch body
1880
- for (const { bodyStart, varNames, lineNo } of catchInsertions) {
1881
- const calls = varNames.map(n => `__trickle_tv(${n},${JSON.stringify(n)},${lineNo})`).join(';');
1882
- allInsertions.push({
1883
- position: bodyStart,
1884
- code: `\ntry{${calls}}catch(__e2){}\n`,
1885
- });
1886
- }
1887
- // For-loop variable insertions: insert trace at start of loop body
1888
- for (const { bodyStart, varNames, lineNo } of forLoopInsertions) {
1889
- const calls = varNames.map(n => `__trickle_tv(${n},${JSON.stringify(n)},${lineNo})`).join(';');
1890
- allInsertions.push({
1891
- position: bodyStart,
1892
- code: `\ntry{${calls}}catch(__e){}\n`,
1893
- });
1894
- }
1895
- // Function parameter insertions: insert trace at start of function body
1896
- for (const { bodyStart, paramNames, lineNo } of funcParamInsertions) {
1897
- const calls = paramNames.map(n => `__trickle_tv(${n},${JSON.stringify(n)},${lineNo})`).join(';');
1898
- allInsertions.push({
1899
- position: bodyStart,
1900
- code: `\ntry{${calls}}catch(__e){}\n`,
1901
- });
1902
- }
1903
- // JSX expression insertions: wrap with comma operator to trace without changing value
1904
- // {expr} → {(__trickle_tv(expr, "expr", lineNo), expr)}
1905
- for (const { exprStart, exprEnd, exprText, lineNo } of jsxExprInsertions) {
1906
- // Use a display name: truncate long expressions, use the raw text
1907
- const displayName = exprText.length > 30 ? exprText.slice(0, 27) + '...' : exprText;
1908
- allInsertions.push({
1909
- position: exprStart,
1910
- code: `(__trickle_tv(`,
1911
- });
1912
- allInsertions.push({
1913
- position: exprEnd,
1914
- code: `,${JSON.stringify(displayName)},${lineNo}),${exprText})`,
1915
- });
1916
- }
1917
- for (const { position, name, lineNo, propsExpr } of bodyInsertions) {
1918
- allInsertions.push({
1919
- position,
1920
- code: `\ntry{__trickle_rc(${JSON.stringify(name)},${lineNo},${propsExpr})}catch(__e){}\n`,
1921
- });
1922
- }
1923
- // Hook insertions: each hook needs TWO insertions (wrapper start before callback, `)` after)
1924
- for (const { wrapStart, wrapEnd, hookName, lineNo } of hookInsertions) {
1925
- allInsertions.push({ position: wrapStart, code: `__trickle_hw(${JSON.stringify(hookName)},${lineNo},` });
1926
- allInsertions.push({ position: wrapEnd, code: `)` });
1927
- }
1928
- // useState insertions: TWO insertions per useState
1929
- // 1. Prefix setter name with '__trickle_s_' (rename in destructuring)
1930
- // 2. After statement end, declare tracked wrapper: const setter = __trickle_ss(...)
1931
- for (const { renamePos, afterLine, stateName, setterName, lineNo } of stateInsertions) {
1932
- allInsertions.push({ position: renamePos, code: `__trickle_s_` });
1933
- allInsertions.push({
1934
- position: afterLine,
1935
- code: `const ${setterName}=__trickle_ss(${JSON.stringify(stateName)},${lineNo},__trickle_s_${setterName});\n`,
1936
- });
1937
- }
1938
- // Concise arrow body insertions: convert `=> (expr)` to `=> { try{__trickle_rc(...)} return (expr); }`
1939
- // Two insertions per component: one before `(`, one after matching `)`
1940
- for (const { beforeParen, afterCloseParen, name, lineNo, propsExpr } of conciseBodyInsertions) {
1941
- allInsertions.push({
1942
- position: beforeParen,
1943
- code: `{ try{__trickle_rc(${JSON.stringify(name)},${lineNo},${propsExpr})}catch(__e){} return `,
1944
- });
1945
- allInsertions.push({
1946
- position: afterCloseParen,
1947
- code: `\n}`,
1948
- });
1949
- }
1950
- // Sort by position descending (insert from end to preserve earlier positions)
1951
- allInsertions.sort((a, b) => b.position - a.position);
1952
- let result = source;
1953
- for (const { position, code } of allInsertions) {
1954
- result = result.slice(0, position) + code + result.slice(position);
1955
- }
1956
- // Preserve 'use client' / 'use server' directives — they must be the first expression
1957
- // in the file (before any imports or code). Extract them from result and prepend before prefix.
1958
- let directive = '';
1959
- const directiveMatch = result.match(/^(\s*(?:'use client'|"use client"|'use server'|"use server")\s*;?\s*\n?)/);
1960
- if (directiveMatch) {
1961
- directive = directiveMatch[1];
1962
- result = result.slice(directiveMatch[0].length);
1963
- }
1964
- return directive + prefix + result;
1965
- }
1966
- export default tricklePlugin;