trickle-observe 0.2.26 → 0.2.27
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/observe-esm-hooks.mjs +237 -5
- package/observe-esm.mjs +1 -0
- package/package.json +1 -1
package/observe-esm-hooks.mjs
CHANGED
|
@@ -17,6 +17,7 @@ let config = {
|
|
|
17
17
|
wrapperPath: '',
|
|
18
18
|
transportPath: '',
|
|
19
19
|
envDetectPath: '',
|
|
20
|
+
traceVarPath: '',
|
|
20
21
|
backendUrl: 'http://localhost:4888',
|
|
21
22
|
debug: false,
|
|
22
23
|
includePatterns: [],
|
|
@@ -171,14 +172,234 @@ function lineOffset(source, lineIdx) {
|
|
|
171
172
|
return off;
|
|
172
173
|
}
|
|
173
174
|
|
|
175
|
+
/**
|
|
176
|
+
* Find variable declarations in ESM source for tracing.
|
|
177
|
+
* Returns {varName, lineNo, insertAfterLine} for each declaration.
|
|
178
|
+
* insertAfterLine = the line number AFTER the full statement ends (for multi-line).
|
|
179
|
+
*/
|
|
180
|
+
function findVarDeclarationsESM(source) {
|
|
181
|
+
const results = [];
|
|
182
|
+
// Match const/let/var <name>[: Type] = <value>
|
|
183
|
+
// Also handles export const <name>[: Type] = <value>
|
|
184
|
+
const varRegex = /^([ \t]*)(?:export\s+)?(const|let|var)\s+([a-zA-Z_$][a-zA-Z0-9_$]*)(?:\s*:[^=]+?)?\s*=[^=]/gm;
|
|
185
|
+
let match;
|
|
186
|
+
|
|
187
|
+
while ((match = varRegex.exec(source)) !== null) {
|
|
188
|
+
const varName = match[3];
|
|
189
|
+
// Skip trickle-injected vars and transpiler-generated vars (start with __ or single _)
|
|
190
|
+
if (varName.startsWith('__') || varName === '_a' || varName === '_b') continue;
|
|
191
|
+
|
|
192
|
+
// Skip require() calls
|
|
193
|
+
const restOfLine = source.slice(match.index + match[0].length - 1, match.index + match[0].length + 200);
|
|
194
|
+
if (/^\s*require\s*\(/.test(restOfLine)) continue;
|
|
195
|
+
|
|
196
|
+
// Calculate line number where the declaration starts
|
|
197
|
+
let lineNo = 1;
|
|
198
|
+
for (let i = 0; i < match.index; i++) {
|
|
199
|
+
if (source[i] === '\n') lineNo++;
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
// Find the end of the statement (semicolon at depth 0 or significant newline)
|
|
203
|
+
const startPos = match.index + match[0].length - 1;
|
|
204
|
+
let pos = startPos;
|
|
205
|
+
let depth = 0;
|
|
206
|
+
let foundEnd = -1;
|
|
207
|
+
|
|
208
|
+
while (pos < source.length) {
|
|
209
|
+
const ch = source[pos];
|
|
210
|
+
if (ch === '(' || ch === '[' || ch === '{') depth++;
|
|
211
|
+
else if (ch === ')' || ch === ']' || ch === '}') {
|
|
212
|
+
depth--;
|
|
213
|
+
if (depth < 0) break;
|
|
214
|
+
} else if (ch === ';' && depth === 0) {
|
|
215
|
+
foundEnd = pos;
|
|
216
|
+
break;
|
|
217
|
+
} else if (ch === '\n' && depth === 0) {
|
|
218
|
+
const nextNonWs = source.slice(pos + 1).match(/^\s*(\S)/);
|
|
219
|
+
if (nextNonWs && !'.+=-|&?:,'.includes(nextNonWs[1])) {
|
|
220
|
+
foundEnd = pos;
|
|
221
|
+
break;
|
|
222
|
+
}
|
|
223
|
+
} else if (ch === '"' || ch === "'" || ch === '`') {
|
|
224
|
+
const quote = ch; pos++;
|
|
225
|
+
while (pos < source.length) {
|
|
226
|
+
if (source[pos] === '\\') pos++;
|
|
227
|
+
else if (source[pos] === quote) break;
|
|
228
|
+
pos++;
|
|
229
|
+
}
|
|
230
|
+
} else if (ch === '/' && pos + 1 < source.length && source[pos + 1] === '/') {
|
|
231
|
+
while (pos < source.length && source[pos] !== '\n') pos++;
|
|
232
|
+
continue;
|
|
233
|
+
} else if (ch === '/' && pos + 1 < source.length && source[pos + 1] === '*') {
|
|
234
|
+
pos += 2;
|
|
235
|
+
while (pos < source.length - 1 && !(source[pos] === '*' && source[pos + 1] === '/')) pos++;
|
|
236
|
+
pos++;
|
|
237
|
+
}
|
|
238
|
+
pos++;
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
if (foundEnd === -1) continue;
|
|
242
|
+
|
|
243
|
+
// Calculate line number after the statement end
|
|
244
|
+
let insertAfterLine = 1;
|
|
245
|
+
for (let i = 0; i <= foundEnd && i < source.length; i++) {
|
|
246
|
+
if (source[i] === '\n') insertAfterLine++;
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
results.push({ varName, lineNo, insertAfterLine });
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
return results;
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
/**
|
|
256
|
+
* Find destructured variable declarations in ESM source.
|
|
257
|
+
*/
|
|
258
|
+
function findDestructuredDeclarationsESM(source) {
|
|
259
|
+
const results = [];
|
|
260
|
+
const destructRegex = /^[ \t]*(?:export\s+)?(?:const|let|var)\s+(\{[^}]*\}|\[[^\]]*\])\s*(?::\s*[^=]+?)?\s*=[^=]/gm;
|
|
261
|
+
let match;
|
|
262
|
+
|
|
263
|
+
while ((match = destructRegex.exec(source)) !== null) {
|
|
264
|
+
const pattern = match[1];
|
|
265
|
+
const varNames = extractDestructuredNamesESM(pattern).filter(n => !n.startsWith('__'));
|
|
266
|
+
if (varNames.length === 0) continue;
|
|
267
|
+
|
|
268
|
+
let lineNo = 1;
|
|
269
|
+
for (let i = 0; i < match.index; i++) {
|
|
270
|
+
if (source[i] === '\n') lineNo++;
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
const startPos = match.index + match[0].length - 1;
|
|
274
|
+
let pos = startPos;
|
|
275
|
+
let depth = 0;
|
|
276
|
+
let foundEnd = -1;
|
|
277
|
+
|
|
278
|
+
while (pos < source.length) {
|
|
279
|
+
const ch = source[pos];
|
|
280
|
+
if (ch === '(' || ch === '[' || ch === '{') depth++;
|
|
281
|
+
else if (ch === ')' || ch === ']' || ch === '}') {
|
|
282
|
+
depth--;
|
|
283
|
+
if (depth < 0) break;
|
|
284
|
+
} else if (ch === ';' && depth === 0) {
|
|
285
|
+
foundEnd = pos;
|
|
286
|
+
break;
|
|
287
|
+
} else if (ch === '\n' && depth === 0) {
|
|
288
|
+
const nextNonWs = source.slice(pos + 1).match(/^\s*(\S)/);
|
|
289
|
+
if (nextNonWs && !'.+=-|&?:,'.includes(nextNonWs[1])) {
|
|
290
|
+
foundEnd = pos;
|
|
291
|
+
break;
|
|
292
|
+
}
|
|
293
|
+
} else if (ch === '"' || ch === "'" || ch === '`') {
|
|
294
|
+
const quote = ch; pos++;
|
|
295
|
+
while (pos < source.length) {
|
|
296
|
+
if (source[pos] === '\\') pos++;
|
|
297
|
+
else if (source[pos] === quote) break;
|
|
298
|
+
pos++;
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
pos++;
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
if (foundEnd === -1) continue;
|
|
305
|
+
|
|
306
|
+
let insertAfterLine = 1;
|
|
307
|
+
for (let i = 0; i <= foundEnd && i < source.length; i++) {
|
|
308
|
+
if (source[i] === '\n') insertAfterLine++;
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
results.push({ varNames, lineNo, insertAfterLine });
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
return results;
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
function extractDestructuredNamesESM(pattern) {
|
|
318
|
+
const names = [];
|
|
319
|
+
const inner = pattern.slice(1, -1).trim();
|
|
320
|
+
if (!inner) return names;
|
|
321
|
+
|
|
322
|
+
const parts = [];
|
|
323
|
+
let depth = 0, current = '';
|
|
324
|
+
for (const ch of inner) {
|
|
325
|
+
if (ch === '{' || ch === '[') depth++;
|
|
326
|
+
else if (ch === '}' || ch === ']') depth--;
|
|
327
|
+
else if (ch === ',' && depth === 0) { parts.push(current.trim()); current = ''; continue; }
|
|
328
|
+
current += ch;
|
|
329
|
+
}
|
|
330
|
+
if (current.trim()) parts.push(current.trim());
|
|
331
|
+
|
|
332
|
+
for (const part of parts) {
|
|
333
|
+
if (part.startsWith('...')) {
|
|
334
|
+
const n = part.slice(3).trim().split(/[\s:]/)[0];
|
|
335
|
+
if (/^[a-zA-Z_$][a-zA-Z0-9_$]*$/.test(n)) names.push(n);
|
|
336
|
+
continue;
|
|
337
|
+
}
|
|
338
|
+
const colonIdx = part.indexOf(':');
|
|
339
|
+
if (colonIdx !== -1) {
|
|
340
|
+
const after = part.slice(colonIdx + 1).trim();
|
|
341
|
+
if (after.startsWith('{') || after.startsWith('[')) {
|
|
342
|
+
names.push(...extractDestructuredNamesESM(after));
|
|
343
|
+
} else {
|
|
344
|
+
const n = after.split(/[\s=]/)[0].trim();
|
|
345
|
+
if (/^[a-zA-Z_$][a-zA-Z0-9_$]*$/.test(n)) names.push(n);
|
|
346
|
+
}
|
|
347
|
+
} else {
|
|
348
|
+
const n = part.split(/[\s=]/)[0].trim();
|
|
349
|
+
if (/^[a-zA-Z_$][a-zA-Z0-9_$]*$/.test(n)) names.push(n);
|
|
350
|
+
}
|
|
351
|
+
}
|
|
352
|
+
return names;
|
|
353
|
+
}
|
|
354
|
+
|
|
174
355
|
function transformSource(source, url) {
|
|
175
356
|
const moduleName = moduleNameFromUrl(url);
|
|
357
|
+
let filePath = url;
|
|
358
|
+
try { filePath = fileURLToPath(url); } catch {}
|
|
359
|
+
|
|
176
360
|
const lines = source.split('\n');
|
|
177
361
|
const exportedFunctions = []; // { name, paramNames }
|
|
178
362
|
const exportedDefaults = []; // { name, paramNames }
|
|
179
363
|
const namedExports = []; // from `export { name }` statements
|
|
180
364
|
const result = [];
|
|
181
365
|
|
|
366
|
+
// Find variable declarations for tracing
|
|
367
|
+
const varTraceEnabled = process.env.TRICKLE_TRACE_VARS !== '0' && config.traceVarPath;
|
|
368
|
+
const varDecls = varTraceEnabled ? findVarDeclarationsESM(source) : [];
|
|
369
|
+
const destructDecls = varTraceEnabled ? findDestructuredDeclarationsESM(source) : [];
|
|
370
|
+
|
|
371
|
+
// Build a map: line number → trace calls to insert AFTER that line
|
|
372
|
+
const traceAfterLine = new Map();
|
|
373
|
+
for (const { varName, lineNo, insertAfterLine } of varDecls) {
|
|
374
|
+
if (!traceAfterLine.has(insertAfterLine)) traceAfterLine.set(insertAfterLine, []);
|
|
375
|
+
traceAfterLine.get(insertAfterLine).push(
|
|
376
|
+
`try{__trickle_tv(${varName},${JSON.stringify(varName)},${lineNo})}catch(__e){}`
|
|
377
|
+
);
|
|
378
|
+
}
|
|
379
|
+
for (const { varNames, lineNo, insertAfterLine } of destructDecls) {
|
|
380
|
+
if (!traceAfterLine.has(insertAfterLine)) traceAfterLine.set(insertAfterLine, []);
|
|
381
|
+
const calls = varNames.map(n => `__trickle_tv(${n},${JSON.stringify(n)},${lineNo})`).join(';');
|
|
382
|
+
traceAfterLine.get(insertAfterLine).push(`try{${calls}}catch(__e){}`);
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
const hasVarTracing = varDecls.length > 0 || destructDecls.length > 0;
|
|
386
|
+
|
|
387
|
+
// Prepend var tracer setup BEFORE user code.
|
|
388
|
+
// In ESM, import declarations are hoisted regardless of position, so
|
|
389
|
+
// `import { createRequire }` here runs before any user code even though
|
|
390
|
+
// it appears before user imports in the source text.
|
|
391
|
+
// The `const` declarations run in order, so they're available when
|
|
392
|
+
// the inline trace calls (injected after var declarations) execute.
|
|
393
|
+
if (hasVarTracing && config.traceVarPath) {
|
|
394
|
+
const tvPath = config.traceVarPath.replace(/\\/g, '\\\\');
|
|
395
|
+
const fpEscaped = filePath.replace(/\\/g, '\\\\');
|
|
396
|
+
result.push(`import { createRequire as __cr_tv } from 'node:module';`);
|
|
397
|
+
result.push(`const __require_tv = __cr_tv(import.meta.url);`);
|
|
398
|
+
result.push(`const __tv_mod = __require_tv('${tvPath}');`);
|
|
399
|
+
result.push(`if (typeof __tv_mod.initVarTracer === 'function') __tv_mod.initVarTracer({});`);
|
|
400
|
+
result.push(`const __trickle_tv = (v, n, l) => { try { __tv_mod.traceVar(v, n, l, ${JSON.stringify(moduleName)}, '${fpEscaped}'); } catch(__e) {} };`);
|
|
401
|
+
}
|
|
402
|
+
|
|
182
403
|
for (let i = 0; i < lines.length; i++) {
|
|
183
404
|
const line = lines[i];
|
|
184
405
|
const trimmed = line.trimStart();
|
|
@@ -274,10 +495,18 @@ function transformSource(source, url) {
|
|
|
274
495
|
|
|
275
496
|
// export class, export type, export interface — leave as-is
|
|
276
497
|
result.push(line);
|
|
498
|
+
|
|
499
|
+
// Inject variable trace calls after this line (if any)
|
|
500
|
+
if (hasVarTracing) {
|
|
501
|
+
const calls = traceAfterLine.get(i + 1); // i+1 = 1-based line number
|
|
502
|
+
if (calls) {
|
|
503
|
+
for (const call of calls) result.push(call);
|
|
504
|
+
}
|
|
505
|
+
}
|
|
277
506
|
}
|
|
278
507
|
|
|
279
|
-
// If nothing to wrap, return original
|
|
280
|
-
if (exportedFunctions.length === 0 && exportedDefaults.length === 0 && namedExports.length === 0) {
|
|
508
|
+
// If nothing to wrap or trace, return original
|
|
509
|
+
if (exportedFunctions.length === 0 && exportedDefaults.length === 0 && namedExports.length === 0 && !hasVarTracing) {
|
|
281
510
|
return source;
|
|
282
511
|
}
|
|
283
512
|
|
|
@@ -323,7 +552,8 @@ function transformSource(source, url) {
|
|
|
323
552
|
|
|
324
553
|
if (config.debug) {
|
|
325
554
|
const fnCount = exportedFunctions.length + exportedDefaults.length + namedExports.length;
|
|
326
|
-
|
|
555
|
+
const varCount = varDecls.length + destructDecls.length;
|
|
556
|
+
console.log(`[trickle/esm] Transformed ${fnCount} exports, ${varCount} vars from ${moduleName}`);
|
|
327
557
|
}
|
|
328
558
|
|
|
329
559
|
return transformed;
|
|
@@ -348,8 +578,10 @@ export async function load(url, context, nextLoad) {
|
|
|
348
578
|
? source
|
|
349
579
|
: Buffer.from(source).toString('utf-8');
|
|
350
580
|
|
|
351
|
-
// Only transform if the module has exports
|
|
352
|
-
|
|
581
|
+
// Only transform if the module has exports or variable declarations to trace
|
|
582
|
+
const varTraceEnabled = process.env.TRICKLE_TRACE_VARS !== '0' && config.traceVarPath;
|
|
583
|
+
const hasVarDecls = varTraceEnabled && /^[ \t]*(?:export\s+)?(?:const|let|var)\s+[a-zA-Z_$]/m.test(sourceStr);
|
|
584
|
+
if (!sourceStr.includes('export ') && !hasVarDecls) return result;
|
|
353
585
|
|
|
354
586
|
try {
|
|
355
587
|
const transformed = transformSource(sourceStr, url);
|
package/observe-esm.mjs
CHANGED
|
@@ -28,6 +28,7 @@ register(pathToFileURL(hooksPath).href, {
|
|
|
28
28
|
wrapperPath: join(__dirname, 'dist', 'wrap.js'),
|
|
29
29
|
transportPath: join(__dirname, 'dist', 'transport.js'),
|
|
30
30
|
envDetectPath: join(__dirname, 'dist', 'env-detect.js'),
|
|
31
|
+
traceVarPath: join(__dirname, 'dist', 'trace-var.js'),
|
|
31
32
|
backendUrl,
|
|
32
33
|
debug,
|
|
33
34
|
includePatterns: process.env.TRICKLE_OBSERVE_INCLUDE
|