@reproapp/node-sdk 0.0.1 → 0.0.2

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.
@@ -0,0 +1,65 @@
1
+ // server.js
2
+ const express = require('express');
3
+ const mongoose = require('mongoose');
4
+
5
+ const app = express();
6
+
7
+ (async () => {
8
+ await mongoose.connect('mongodb://127.0.0.1:27018', { dbName: 'trace_demo' });
9
+
10
+ const userSchema = new mongoose.Schema({
11
+ name: String, email: String, age: Number
12
+ }, { timestamps: true });
13
+ const User = mongoose.model('User', userSchema);
14
+
15
+ // seed some data
16
+ app.get('/seed', async (_req, res) => {
17
+ await User.deleteMany({});
18
+ await User.insertMany([
19
+ { name: 'Ada', email: 'ada@example.com', age: 27 },
20
+ { name: 'Lin', email: 'lin@example.com', age: 35 },
21
+ { name: 'Sam', email: 'sam@example.com', age: 17 },
22
+ ]);
23
+ res.json({ ok: true, count: await User.countDocuments() });
24
+ });
25
+
26
+ // list adults
27
+ app.get('/users', async (_req, res) => {
28
+ const list = await User.find({ age: { $gte: 18 } }).lean().limit(5).exec();
29
+ res.json({ list });
30
+ });
31
+
32
+ // single by id
33
+ app.get('/users/:id', async (req, res) => {
34
+ const u = await User.findById(req.params.id).exec();
35
+ res.json({ u });
36
+ });
37
+
38
+ // aggregate example
39
+ app.get('/agg', async (_req, res) => {
40
+ const out = await User.aggregate([
41
+ { $group: { _id: null, avgAge: { $avg: '$age' }, count: { $sum: 1 } } }
42
+ ]);
43
+ res.json({ agg: out[0] });
44
+ });
45
+
46
+ // native node: fs + crypto + timers
47
+ app.get('/native', async (_req, res) => {
48
+ const fs = require('node:fs/promises');
49
+ const crypto = require('node:crypto');
50
+
51
+ console.log('starting native work');
52
+
53
+ const src = await fs.readFile(__filename, 'utf8');
54
+ await new Promise((resolve, reject) =>
55
+ crypto.pbkdf2('secret', Buffer.alloc(16, 7), 10_000, 32, 'sha256',
56
+ (err, key) => err ? reject(err) : resolve(key))
57
+ );
58
+
59
+ await new Promise(r => setTimeout(r, 20));
60
+
61
+ res.json({ ok: true, size: src.length });
62
+ });
63
+
64
+ app.listen(3000, () => process.stdout.write('listening on http://localhost:3000\n'));
65
+ })();
@@ -0,0 +1,608 @@
1
+ // wrap-plugin.js
2
+ module.exports = function makeWrapPlugin(filenameForMeta, opts = {}) {
3
+ return ({ types: t }) => {
4
+ const {
5
+ mode = 'all', // 'all' | 'allowlist'
6
+ allowFns = [], // regexes or strings
7
+ wrapGettersSetters = false, // skip noisy accessors by default
8
+ skipAnonymous = false, // don't wrap anon fns in node_modules
9
+ mapOriginalPosition = null,
10
+ } = opts;
11
+
12
+ const allowFnRegexes = allowFns.map(p =>
13
+ typeof p === 'string' ? new RegExp(`^${escapeRx(p)}$`) : p
14
+ );
15
+
16
+ function escapeRx(s){ return s.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); }
17
+
18
+ const markInternal = (node) => {
19
+ if (node && typeof node === 'object') {
20
+ node.__repro_internal = true;
21
+ }
22
+ return node;
23
+ };
24
+
25
+ function describeExpression(node, depth = 0) {
26
+ if (!node || depth > 5) return '';
27
+ if (t.isIdentifier(node)) return node.name;
28
+ if (t.isThisExpression(node)) return 'this';
29
+ if (t.isSuper(node)) return 'super';
30
+ if (t.isStringLiteral(node)) return node.value;
31
+ if (t.isNumericLiteral(node)) return String(node.value);
32
+ if (t.isMemberExpression(node)) {
33
+ const obj = describeExpression(node.object, depth + 1) || '';
34
+ let prop = '';
35
+ if (!node.computed && t.isIdentifier(node.property)) {
36
+ prop = node.property.name;
37
+ } else if (!node.computed && t.isStringLiteral(node.property)) {
38
+ prop = node.property.value;
39
+ } else if (!node.computed && t.isNumericLiteral(node.property)) {
40
+ prop = String(node.property.value);
41
+ } else {
42
+ const inner = describeExpression(node.property, depth + 1) || '?';
43
+ prop = node.computed ? `[${inner}]` : inner;
44
+ }
45
+ if (!obj) return prop || '';
46
+ if (prop.startsWith('[')) return `${obj}${prop}`;
47
+ return prop ? `${obj}.${prop}` : obj;
48
+ }
49
+ return '';
50
+ }
51
+
52
+ const obj = kv => t.objectExpression(
53
+ Object.entries(kv)
54
+ .filter(([, v]) => v != null)
55
+ .map(([k, v]) => {
56
+ let valueNode;
57
+ if (typeof v === 'string') valueNode = t.stringLiteral(v);
58
+ else if (typeof v === 'number') valueNode = t.numericLiteral(v);
59
+ else if (typeof v === 'boolean') valueNode = t.booleanLiteral(v);
60
+ else if (v && typeof v === 'object' && v.type) valueNode = v;
61
+ else if (v === null) valueNode = t.nullLiteral();
62
+ else throw new Error(`Unsupported object literal value for key ${k}`);
63
+ return t.objectProperty(t.identifier(k), valueNode);
64
+ })
65
+ );
66
+
67
+ function nameFor(path){
68
+ const n = path.node;
69
+ if (n.id?.name) return n.id.name;
70
+ if ((path.isClassMethod() || path.isObjectMethod()) && n.key) {
71
+ if (t.isIdentifier(n.key)) return n.key.name;
72
+ if (t.isStringLiteral(n.key)) return n.key.value;
73
+ if (t.isNumericLiteral(n.key)) return String(n.key.value);
74
+ }
75
+ if (path.parentPath?.isVariableDeclarator() && t.isIdentifier(path.parentPath.node.id))
76
+ return path.parentPath.node.id.name;
77
+ if (path.parentPath?.isAssignmentExpression()) {
78
+ const left = path.parentPath.node.left;
79
+ if (t.isIdentifier(left)) return left.name;
80
+ if (t.isMemberExpression(left)) {
81
+ const p = left.property;
82
+ if (t.isIdentifier(p)) return p.name;
83
+ if (t.isStringLiteral(p)) return p.value;
84
+ if (t.isNumericLiteral(p)) return String(p.value);
85
+ }
86
+ }
87
+ // Heuristic: generator passed to __awaiter — treat it as the enclosing function’s name
88
+ if (path.parentPath?.isCallExpression() &&
89
+ t.isIdentifier(path.parentPath.node.callee, { name: '__awaiter' })) {
90
+ const enclosing = path.getFunctionParent();
91
+ if (enclosing && enclosing.node !== path.node) {
92
+ const enclosingName = nameFor(enclosing);
93
+ if (enclosingName && enclosingName !== '(anonymous)') return enclosingName;
94
+ }
95
+ }
96
+ return '(anonymous)';
97
+ }
98
+
99
+ function classifyFunction(path){
100
+ const n = path.node;
101
+
102
+ if (path.isClassMethod() || path.isClassPrivateMethod?.()) {
103
+ if (n.kind === 'constructor') return 'constructor';
104
+ if (n.kind === 'get') return 'getter';
105
+ if (n.kind === 'set') return 'setter';
106
+ if (n.static) return 'static-method';
107
+ return 'method';
108
+ }
109
+
110
+ if (path.isObjectMethod && path.isObjectMethod()) {
111
+ if (n.kind === 'get') return 'getter';
112
+ if (n.kind === 'set') return 'setter';
113
+ return 'method';
114
+ }
115
+
116
+ if (path.isClassPrivateProperty?.()) return 'method';
117
+
118
+ if (path.isArrowFunctionExpression()) return 'arrow';
119
+
120
+ if (path.isFunctionDeclaration()) return 'function';
121
+ if (path.isFunctionExpression()) {
122
+ if (path.parentPath?.isClassProperty?.()) return 'method';
123
+ return 'function';
124
+ }
125
+
126
+ return 'function';
127
+ }
128
+
129
+ function shouldWrap(path, name){
130
+ // skip getters/setters unless asked
131
+ if (!wrapGettersSetters &&
132
+ (path.node.kind === 'get' || path.node.kind === 'set')) return false;
133
+
134
+ if (skipAnonymous && name === '(anonymous)') return false;
135
+
136
+ if (mode === 'allowlist') {
137
+ return allowFnRegexes.length === 0
138
+ ? false
139
+ : allowFnRegexes.some(rx => rx.test(name));
140
+ }
141
+ return true; // mode 'all'
142
+ }
143
+
144
+ const TS_HELPER_NAMES = new Set(['__awaiter', '__generator']);
145
+
146
+ function isAwaiterBody(path) {
147
+ if (!path.parentPath) return false;
148
+ if (!path.parentPath.isCallExpression()) return false;
149
+ const call = path.parentPath.node;
150
+ const callee = call.callee;
151
+ const isHelper = t.isIdentifier(callee, { name: '__awaiter' }) || t.isIdentifier(callee, { name: '__generator' });
152
+ if (!isHelper) return false;
153
+ if (path.listKey !== 'arguments') return false;
154
+ const argIndex = typeof path.key === 'number' ? path.key : -1;
155
+ return argIndex === call.arguments.length - 1;
156
+ }
157
+
158
+ function wrap(path){
159
+ const n = path.node;
160
+ if (n.__repro_internal) return;
161
+ if (n.__wrapped) return;
162
+
163
+ if (isAwaiterBody(path)) {
164
+ // Don't wrap the generator passed to __awaiter/__generator; we still want to instrument its callsites.
165
+ return;
166
+ }
167
+ const name = nameFor(path);
168
+ if (TS_HELPER_NAMES.has(name)) {
169
+ markInternal(n);
170
+ path.skip(); // don’t instrument TS async helpers or their internals
171
+ return;
172
+ }
173
+ if (!shouldWrap(path, name)) return;
174
+
175
+ const loc = n.loc?.start || null;
176
+ const mapped = loc && typeof mapOriginalPosition === 'function'
177
+ ? mapOriginalPosition(loc.line ?? null, loc.column ?? 0)
178
+ : null;
179
+
180
+ const file = mapped?.file || filenameForMeta;
181
+ const line = mapped?.line ?? loc?.line ?? null;
182
+ const fnType = classifyFunction(path);
183
+
184
+ if (t.isArrowFunctionExpression(n) && !t.isBlockStatement(n.body)) {
185
+ const bodyExprPath = path.get('body');
186
+ const origBody = t.cloneNode(bodyExprPath.node, true);
187
+ bodyExprPath.replaceWith(t.blockStatement([ t.returnStatement(origBody) ]));
188
+ }
189
+
190
+ const bodyPath = path.get('body');
191
+ if (!bodyPath.isBlockStatement()) return;
192
+ const body = bodyPath.node;
193
+
194
+ const argsId = path.scope.generateUidIdentifier('args');
195
+ const resultId = path.scope.generateUidIdentifier('result');
196
+ const errorId = path.scope.generateUidIdentifier('error');
197
+ const threwId = path.scope.generateUidIdentifier('threw');
198
+
199
+ const typeofArgs = t.unaryExpression('typeof', t.identifier('arguments'));
200
+ const arrayProto = t.memberExpression(t.identifier('Array'), t.identifier('prototype'));
201
+ const arraySlice = t.memberExpression(arrayProto, t.identifier('slice'));
202
+ const sliceCall = t.memberExpression(arraySlice, t.identifier('call'));
203
+ const argsArray = markInternal(t.callExpression(sliceCall, [ t.identifier('arguments') ]));
204
+ const canUseArguments = !path.isArrowFunctionExpression();
205
+ const argsInit = canUseArguments
206
+ ? t.conditionalExpression(
207
+ t.binaryExpression('===', typeofArgs, t.stringLiteral('undefined')),
208
+ t.arrayExpression([]),
209
+ argsArray
210
+ )
211
+ : t.arrayExpression(
212
+ n.params.map(param => {
213
+ if (t.isIdentifier(param)) return t.cloneNode(param, true);
214
+ if (t.isRestElement(param) && t.isIdentifier(param.argument)) {
215
+ return t.cloneNode(param.argument, true);
216
+ }
217
+ if (t.isAssignmentPattern(param) && t.isIdentifier(param.left)) {
218
+ return t.cloneNode(param.left, true);
219
+ }
220
+ return t.identifier('undefined');
221
+ })
222
+ );
223
+
224
+ const argsDecl = t.variableDeclaration('const', [
225
+ t.variableDeclarator(argsId, argsInit)
226
+ ]);
227
+
228
+ const localsDecl = t.variableDeclaration('let', [
229
+ t.variableDeclarator(resultId, t.identifier('undefined')),
230
+ t.variableDeclarator(errorId, t.nullLiteral()),
231
+ t.variableDeclarator(threwId, t.booleanLiteral(false))
232
+ ]);
233
+
234
+ const fnIdForMark = (path.isFunctionDeclaration() || path.isFunctionExpression()) && n.id && t.isIdentifier(n.id)
235
+ ? n.id
236
+ : null;
237
+ const markBodyTraced = fnIdForMark
238
+ ? markInternal(t.expressionStatement(
239
+ t.assignmentExpression('=', t.memberExpression(t.identifier(fnIdForMark.name), t.identifier('__repro_body_traced')), t.booleanLiteral(true))
240
+ ))
241
+ : null;
242
+
243
+ const enter = t.expressionStatement(
244
+ markInternal(t.callExpression(
245
+ t.memberExpression(t.identifier('__trace'), t.identifier('enter')),
246
+ [ t.stringLiteral(name), obj({ file, line, functionType: fnType }), obj({ args: argsId }) ]
247
+ ))
248
+ );
249
+
250
+ const exit = t.expressionStatement(
251
+ markInternal(t.callExpression(
252
+ t.memberExpression(t.identifier('__trace'), t.identifier('exit')),
253
+ [
254
+ obj({ fn: name, file, line, functionType: fnType }),
255
+ obj({ returnValue: resultId, error: errorId, threw: threwId, args: argsId })
256
+ ]
257
+ ))
258
+ );
259
+
260
+ const errId = path.scope.generateUidIdentifier('err');
261
+ bodyPath.traverse({
262
+ ReturnStatement(retPath) {
263
+ if (retPath.node.__repro_wrapped) return;
264
+ if (retPath.getFunctionParent() !== path) return;
265
+ const arg = retPath.node.argument
266
+ ? t.cloneNode(retPath.node.argument, true)
267
+ : t.identifier('undefined');
268
+ const seq = t.sequenceExpression([
269
+ t.assignmentExpression('=', resultId, arg),
270
+ resultId
271
+ ]);
272
+ const newReturn = t.returnStatement(seq);
273
+ newReturn.__repro_wrapped = true;
274
+ retPath.replaceWith(newReturn);
275
+ }
276
+ });
277
+
278
+ const wrappedTry = t.tryStatement(
279
+ body,
280
+ t.catchClause(errId, t.blockStatement([
281
+ t.expressionStatement(
282
+ t.assignmentExpression('=', threwId, t.booleanLiteral(true))
283
+ ),
284
+ t.expressionStatement(
285
+ t.assignmentExpression('=', errorId, errId)
286
+ ),
287
+ t.throwStatement(errId)
288
+ ])),
289
+ t.blockStatement([ exit ])
290
+ );
291
+
292
+ const prologue = markBodyTraced ? [ markBodyTraced, argsDecl, localsDecl, enter ] : [ argsDecl, localsDecl, enter ];
293
+ const wrapped = t.blockStatement([ ...prologue, wrappedTry ]);
294
+
295
+ if (path.isFunction() || path.isClassMethod() || path.isObjectMethod()) {
296
+ bodyPath.replaceWith(wrapped);
297
+ }
298
+ n.__wrapped = true;
299
+ }
300
+
301
+ function isSkippableParent(p) {
302
+ if (!p) return false;
303
+ if (p.isParenthesizedExpression && p.isParenthesizedExpression()) return true;
304
+ if (p.isTSAsExpression && p.isTSAsExpression()) return true;
305
+ if (p.isTSTypeAssertion && p.isTSTypeAssertion()) return true;
306
+ if (p.isTSNonNullExpression && p.isTSNonNullExpression()) return true;
307
+ if (p.isTypeCastExpression && p.isTypeCastExpression()) return true;
308
+ if (p.isSequenceExpression && p.isSequenceExpression()) return true;
309
+ if (p.isConditionalExpression && p.isConditionalExpression()) return true;
310
+ if (p.isLogicalExpression && p.isLogicalExpression()) return true;
311
+ if (p.isBinaryExpression && p.isBinaryExpression()) return true;
312
+ if (p.isUnaryExpression && p.isUnaryExpression()) return true;
313
+ if (p.isArrayExpression && p.isArrayExpression()) return true;
314
+ if (p.isObjectExpression && p.isObjectExpression()) return true;
315
+ if (p.isObjectProperty && p.isObjectProperty()) return true;
316
+ if (p.isObjectMethod && p.isObjectMethod()) return true;
317
+ if (p.isSpreadElement && p.isSpreadElement()) return true;
318
+ if (p.isNewExpression && p.isNewExpression()) return true;
319
+ if (p.isMemberExpression && p.isMemberExpression()) return true;
320
+ if (p.isOptionalMemberExpression && p.isOptionalMemberExpression()) return true;
321
+ if (p.isTaggedTemplateExpression && p.isTaggedTemplateExpression()) return true;
322
+ if (p.isTemplateLiteral && p.isTemplateLiteral()) return true;
323
+ return false;
324
+ }
325
+
326
+ function isAwaitedCall(path) {
327
+ let current = path;
328
+ const funcParent = path.getFunctionParent();
329
+
330
+ while (current && current.parentPath) {
331
+ const parent = current.parentPath;
332
+
333
+ if (parent.isAwaitExpression && parent.isAwaitExpression()) return true;
334
+ if (parent.isYieldExpression && parent.isYieldExpression()) return true;
335
+
336
+ if (parent.isReturnStatement && parent.isReturnStatement()) {
337
+ if (parent.node.argument === current.node &&
338
+ funcParent &&
339
+ (funcParent.node.async || funcParent.node.generator)) {
340
+ return true;
341
+ }
342
+ }
343
+
344
+ if (parent.isForOfStatement && parent.isForOfStatement()) {
345
+ if (parent.node.await === true && parent.node.right === current.node) {
346
+ return true;
347
+ }
348
+ }
349
+
350
+ if (!isSkippableParent(parent)) break;
351
+ current = parent;
352
+ }
353
+
354
+ return false;
355
+ }
356
+
357
+ // ---- NEW: wrap every call-site with __repro_call(...) ----
358
+ function wrapCall(path, state) {
359
+ const { node: n } = path;
360
+ if (n.__repro_call_wrapped) return;
361
+ if (n.__repro_internal) return;
362
+
363
+ // Skip our helper, super(), import(), optional calls for now
364
+ if (t.isIdentifier(n.callee, { name: '__repro_call' })) return;
365
+ if (t.isIdentifier(n.callee, { name: '__awaiter' })) return;
366
+ if (t.isIdentifier(n.callee, { name: '__generator' })) return;
367
+ if (t.isSuper(n.callee)) return;
368
+ if (t.isImport(n.callee)) return;
369
+
370
+ if (t.isMemberExpression(n.callee) && t.isIdentifier(n.callee.object, { name: '__trace' })) {
371
+ return;
372
+ }
373
+ if (t.isIdentifier(n.callee, { name: '__trace' })) return;
374
+
375
+ const loc = n.loc?.start || null;
376
+ const mapped = loc && typeof mapOriginalPosition === 'function'
377
+ ? mapOriginalPosition(loc.line ?? null, loc.column ?? 0)
378
+ : null;
379
+
380
+ const file = mapped?.file || filenameForMeta || state.file.opts.filename || '';
381
+ const line = mapped?.line ?? loc?.line ?? 0;
382
+
383
+ const fileLit = t.stringLiteral(file ?? '');
384
+ const lineLit = t.numericLiteral(line ?? 0);
385
+
386
+ const unawaited = !isAwaitedCall(path);
387
+
388
+ // Default: no thisArg, label from identifier name if any
389
+ let label = '';
390
+ let callExpr;
391
+
392
+ if (t.isMemberExpression(n.callee)) {
393
+ // --- Member call: obj.method(...args)
394
+ // Hoist obj and fn into temps so obj is evaluated ONCE.
395
+ const objOrig = n.callee.object;
396
+ const prop = n.callee.property;
397
+ const computed = n.callee.computed === true;
398
+
399
+ const objId = path.scope.generateUidIdentifierBasedOnNode(objOrig, 'obj');
400
+ const fnId = path.scope.generateUidIdentifier('fn');
401
+
402
+ // label = property name when available
403
+ label = describeExpression(n.callee) || label;
404
+
405
+ const fnMember = t.memberExpression(objId, prop, computed);
406
+ const argsArray = t.arrayExpression(n.arguments.map(arg => t.cloneNode(arg, true)));
407
+
408
+ const reproCall = t.callExpression(
409
+ t.identifier('__repro_call'),
410
+ [
411
+ fnId,
412
+ objId,
413
+ argsArray,
414
+ fileLit,
415
+ lineLit,
416
+ t.stringLiteral(label || ''),
417
+ t.booleanLiteral(unawaited)
418
+ ]
419
+ );
420
+
421
+ // Build a single expression that:
422
+ // const _obj = (origObj), _fn = _obj.prop, __repro_call(_fn, _obj, args, ...)
423
+ // We use a sequence expression so it works anywhere an expression is allowed.
424
+ callExpr = t.sequenceExpression([
425
+ t.assignmentExpression('=', objId, objOrig),
426
+ t.assignmentExpression('=', fnId, fnMember),
427
+ reproCall
428
+ ]);
429
+
430
+ // Ensure the temps are declared in the current scope
431
+ path.scope.push({ id: objId });
432
+ path.scope.push({ id: fnId });
433
+ } else {
434
+ // --- Plain call: fn(...args)
435
+ // Evaluate callee ONCE into a temp as well (avoids re-evaluation when nested).
436
+ const fnOrig = n.callee;
437
+ const fnId = path.scope.generateUidIdentifier('fn');
438
+ const argsArray = t.arrayExpression(n.arguments.map(arg => t.cloneNode(arg, true)));
439
+
440
+ label = describeExpression(fnOrig) || label;
441
+
442
+ const reproCall = t.callExpression(
443
+ t.identifier('__repro_call'),
444
+ [
445
+ fnId,
446
+ t.nullLiteral(),
447
+ argsArray,
448
+ fileLit,
449
+ lineLit,
450
+ t.stringLiteral(label || ''),
451
+ t.booleanLiteral(unawaited)
452
+ ]
453
+ );
454
+
455
+ callExpr = t.sequenceExpression([
456
+ t.assignmentExpression('=', fnId, fnOrig),
457
+ reproCall
458
+ ]);
459
+
460
+ path.scope.push({ id: fnId });
461
+ }
462
+
463
+ path.replaceWith(callExpr);
464
+ path.node.__repro_call_wrapped = true;
465
+ }
466
+
467
+ function wrapOptionalCall(path, state) {
468
+ const { node: n } = path;
469
+ if (n.__repro_call_wrapped) return;
470
+ if (n.__repro_internal) return;
471
+
472
+ if (t.isIdentifier(n.callee, { name: '__repro_call' })) return;
473
+ if (t.isIdentifier(n.callee, { name: '__awaiter' })) return;
474
+ if (t.isIdentifier(n.callee, { name: '__generator' })) return;
475
+ if (t.isSuper(n.callee)) return;
476
+ if (t.isImport(n.callee)) return;
477
+
478
+ const loc = n.loc?.start || null;
479
+ const mapped = loc && typeof mapOriginalPosition === 'function'
480
+ ? mapOriginalPosition(loc.line ?? null, loc.column ?? 0)
481
+ : null;
482
+
483
+ const file = mapped?.file || filenameForMeta || state.file.opts.filename || '';
484
+ const line = mapped?.line ?? loc?.line ?? 0;
485
+
486
+ const fileLit = t.stringLiteral(file ?? '');
487
+ const lineLit = t.numericLiteral(line ?? 0);
488
+
489
+ const unawaited = !isAwaitedCall(path);
490
+ const label = describeExpression(n.callee) || '';
491
+ const makeArgsArray = () => t.arrayExpression(n.arguments.map(arg => t.cloneNode(arg, true)));
492
+
493
+ const isOptMember = (t.isOptionalMemberExpression && t.isOptionalMemberExpression(n.callee)) || false;
494
+ if (t.isMemberExpression(n.callee) || isOptMember) {
495
+ const callee = n.callee;
496
+ const objOrig = callee.object;
497
+ const prop = callee.property;
498
+ const computed = callee.computed === true;
499
+
500
+ const needsObjGuard = Boolean(callee.optional);
501
+ const needsCalleeGuard = n.optional === true;
502
+
503
+ const objId = path.scope.generateUidIdentifierBasedOnNode(objOrig, 'obj');
504
+ const fnId = path.scope.generateUidIdentifier('fn');
505
+
506
+ const member = t.memberExpression(objId, prop, computed);
507
+ const fnInit = needsObjGuard
508
+ ? t.conditionalExpression(
509
+ t.binaryExpression('==', objId, t.nullLiteral()),
510
+ t.identifier('undefined'),
511
+ member
512
+ )
513
+ : member;
514
+
515
+ const tests = [];
516
+ if (needsObjGuard) tests.push(t.binaryExpression('==', objId, t.nullLiteral()));
517
+ if (needsCalleeGuard) tests.push(t.binaryExpression('==', fnId, t.nullLiteral()));
518
+
519
+ let guardTest = null;
520
+ if (tests.length === 1) guardTest = tests[0];
521
+ else if (tests.length === 2) guardTest = t.logicalExpression('||', tests[0], tests[1]);
522
+
523
+ const reproCall = t.callExpression(
524
+ t.identifier('__repro_call'),
525
+ [
526
+ fnId,
527
+ objId,
528
+ makeArgsArray(),
529
+ fileLit,
530
+ lineLit,
531
+ t.stringLiteral(label || ''),
532
+ t.booleanLiteral(unawaited)
533
+ ]
534
+ );
535
+
536
+ const guardedCall = guardTest
537
+ ? t.conditionalExpression(guardTest, t.identifier('undefined'), reproCall)
538
+ : reproCall;
539
+
540
+ const seq = t.sequenceExpression([
541
+ t.assignmentExpression('=', objId, objOrig),
542
+ t.assignmentExpression('=', fnId, fnInit),
543
+ guardedCall
544
+ ]);
545
+
546
+ path.scope.push({ id: objId });
547
+ path.scope.push({ id: fnId });
548
+ path.replaceWith(seq);
549
+ path.node.__repro_call_wrapped = true;
550
+ return;
551
+ }
552
+
553
+ const fnOrig = n.callee;
554
+ const fnId = path.scope.generateUidIdentifier('fn');
555
+ const needsCalleeGuard = n.optional === true;
556
+ const guardTest = needsCalleeGuard ? t.binaryExpression('==', fnId, t.nullLiteral()) : null;
557
+
558
+ const reproCall = t.callExpression(
559
+ t.identifier('__repro_call'),
560
+ [
561
+ fnId,
562
+ t.nullLiteral(),
563
+ makeArgsArray(),
564
+ fileLit,
565
+ lineLit,
566
+ t.stringLiteral(label || ''),
567
+ t.booleanLiteral(unawaited)
568
+ ]
569
+ );
570
+
571
+ const guardedCall = guardTest
572
+ ? t.conditionalExpression(guardTest, t.identifier('undefined'), reproCall)
573
+ : reproCall;
574
+
575
+ const seq = t.sequenceExpression([
576
+ t.assignmentExpression('=', fnId, fnOrig),
577
+ guardedCall
578
+ ]);
579
+
580
+ path.scope.push({ id: fnId });
581
+ path.replaceWith(seq);
582
+ path.node.__repro_call_wrapped = true;
583
+ }
584
+
585
+ return {
586
+ name: 'omnitrace-wrap-functions-and-calls',
587
+ visitor: {
588
+ // function body enter/exit
589
+ FunctionDeclaration: wrap,
590
+ FunctionExpression: wrap,
591
+ ArrowFunctionExpression: wrap,
592
+ ObjectMethod: wrap,
593
+ ClassMethod: wrap,
594
+ ClassPrivateMethod: wrap,
595
+
596
+ // call-site wrapping
597
+ CallExpression: {
598
+ exit(path, state) { wrapCall(path, state); }
599
+ },
600
+ OptionalCallExpression: {
601
+ exit(path, state) { wrapOptionalCall(path, state); }
602
+ },
603
+ // (If you also want to wrap OptionalCallExpression in older Babel ASTs,
604
+ // add the same handler here)
605
+ }
606
+ };
607
+ };
608
+ };
package/tsconfig.json ADDED
@@ -0,0 +1,12 @@
1
+ {
2
+ "compilerOptions": {
3
+ "target": "ES2020",
4
+ "module": "CommonJS",
5
+ "declaration": true,
6
+ "outDir": "dist",
7
+ "rootDir": "src",
8
+ "strict": true,
9
+ "skipLibCheck": true
10
+ },
11
+ "include": ["src"]
12
+ }