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.
- package/package.json +1 -1
- package/dist-esm/vite-plugin.js +0 -1966
package/dist-esm/vite-plugin.js
DELETED
|
@@ -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;
|