@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,184 @@
1
+ // repro-node/src/integrations/sendgrid.ts
2
+ import { AsyncLocalStorage } from 'async_hooks';
3
+
4
+ type Ctx = { sid?: string; aid?: string };
5
+ const als = new AsyncLocalStorage<Ctx>();
6
+ export const getCtx = () => als.getStore() || {};
7
+
8
+ // If you already export als/getCtx from repro-node, reuse that instead of re-declaring.
9
+
10
+ async function post(
11
+ cfg: { appId: string; appSecret: string; appName?: string; apiBase?: string },
12
+ sessionId: string,
13
+ body: any,
14
+ ) {
15
+ try {
16
+ const envBase = typeof process !== 'undefined' ? (process as any)?.env?.REPRO_API_BASE : undefined;
17
+ const legacyBase = (cfg as any)?.apiBase;
18
+ const apiBase = String(envBase || legacyBase || 'https://oozy-loreta-gully.ngrok-free.dev').replace(/\/+$/, '');
19
+ await fetch(`${apiBase}/v1/sessions/${sessionId}/backend`, {
20
+ method: 'POST',
21
+ headers: {
22
+ 'Content-Type': 'application/json',
23
+ 'X-App-Id': cfg.appId,
24
+ 'X-App-Secret': cfg.appSecret,
25
+ ...(cfg.appName ? { 'X-App-Name': cfg.appName } : {}),
26
+ },
27
+ body: JSON.stringify(body),
28
+ });
29
+ } catch { /* swallow */ }
30
+ }
31
+
32
+ export type SendgridPatchConfig = {
33
+ appId: string;
34
+ appSecret: string;
35
+ appName?: string;
36
+ // Optional: provide a function to resolve sid/aid if AsyncLocalStorage is not set
37
+ resolveContext?: () => { sid?: string; aid?: string } | undefined;
38
+ };
39
+
40
+ /**
41
+ * Patch @sendgrid/mail's send() and sendMultiple() to capture outbound email.
42
+ * Idempotent. No behavior change for the app.
43
+ */
44
+ export function patchSendgridMail(cfg: SendgridPatchConfig) {
45
+ let sgMail: any;
46
+ try { sgMail = require('@sendgrid/mail'); } catch { return; } // not installed → no-op
47
+
48
+ if (!sgMail || (sgMail as any).__repro_patched) return;
49
+ (sgMail as any).__repro_patched = true;
50
+
51
+ const origSend = sgMail.send?.bind(sgMail);
52
+ const origSendMultiple = sgMail.sendMultiple?.bind(sgMail);
53
+
54
+ if (origSend) {
55
+ sgMail.send = async function patchedSend(msg: any, isMultiple?: boolean) {
56
+ const startedAt = Date.now();
57
+ let statusCode: number | undefined;
58
+ let headers: Record<string, any> | undefined;
59
+
60
+ try {
61
+ const res = await origSend(msg, isMultiple);
62
+ // sendgrid returns [response] as array in v7.x
63
+ const r = Array.isArray(res) ? res[0] : res;
64
+ statusCode = r?.statusCode ?? r?.status ?? undefined;
65
+ headers = r?.headers ?? undefined;
66
+ return res;
67
+ } finally {
68
+ fireCapture('send', msg, startedAt, statusCode, headers);
69
+ }
70
+ };
71
+ }
72
+
73
+ if (origSendMultiple) {
74
+ sgMail.sendMultiple = async function patchedSendMultiple(msg: any) {
75
+ const startedAt = Date.now();
76
+ let statusCode: number | undefined;
77
+ let headers: Record<string, any> | undefined;
78
+
79
+ try {
80
+ const res = await origSendMultiple(msg);
81
+ const r = Array.isArray(res) ? res[0] : res;
82
+ statusCode = r?.statusCode ?? r?.status ?? undefined;
83
+ headers = r?.headers ?? undefined;
84
+ return res;
85
+ } finally {
86
+ fireCapture('sendMultiple', msg, startedAt, statusCode, headers);
87
+ }
88
+ };
89
+ }
90
+
91
+ function fireCapture(kind: 'send' | 'sendMultiple', rawMsg: any, t0: number, statusCode?: number, headers?: any) {
92
+ const ctx = getCtx();
93
+ const sid = ctx.sid ?? cfg.resolveContext?.()?.sid;
94
+ const aid = ctx.aid ?? cfg.resolveContext?.()?.aid;
95
+ if (!sid) return; // no active session → skip
96
+
97
+ const norm = normalizeSendgridMessage(rawMsg);
98
+ const entry = {
99
+ actionId: aid ?? null,
100
+ email: {
101
+ provider: 'sendgrid',
102
+ kind,
103
+ to: norm.to,
104
+ cc: norm.cc,
105
+ bcc: norm.bcc,
106
+ from: norm.from,
107
+ subject: norm.subject,
108
+ text: norm.text, // you said privacy later → include now
109
+ html: norm.html, // idem
110
+ templateId: norm.templateId,
111
+ dynamicTemplateData: norm.dynamicTemplateData,
112
+ categories: norm.categories,
113
+ customArgs: norm.customArgs,
114
+ attachmentsMeta: norm.attachmentsMeta, // safe metadata only
115
+ statusCode,
116
+ durMs: Date.now() - t0,
117
+ headers: headers ?? {},
118
+ },
119
+ t: Date.now(),
120
+ };
121
+
122
+ post(cfg, sid, { entries: [entry] });
123
+ }
124
+
125
+ function normalizeAddress(a: any): { email: string; name?: string } | null {
126
+ if (!a) return null;
127
+ if (typeof a === 'string') return { email: a };
128
+ if (typeof a === 'object' && a.email) return { email: String(a.email), name: a.name ? String(a.name) : undefined };
129
+ return null;
130
+ }
131
+
132
+ function normalizeAddressList(v: any): Array<{ email: string; name?: string }> | undefined {
133
+ if (!v) return undefined;
134
+ const arr = Array.isArray(v) ? v : [v];
135
+ const out = arr.map(normalizeAddress).filter(Boolean) as Array<{ email: string; name?: string }>;
136
+ return out.length ? out : undefined;
137
+ }
138
+
139
+ function normalizeSendgridMessage(msg: any) {
140
+ // sendgrid supports "personalizations" & top-level fields; we’ll flatten the common pieces
141
+ const base = {
142
+ from: normalizeAddress(msg?.from) ?? undefined,
143
+ to: normalizeAddressList(msg?.to),
144
+ cc: normalizeAddressList(msg?.cc),
145
+ bcc: normalizeAddressList(msg?.bcc),
146
+ subject: msg?.subject ? String(msg.subject) : undefined,
147
+ text: typeof msg?.text === 'string' ? msg.text : undefined,
148
+ html: typeof msg?.html === 'string' ? msg.html : undefined,
149
+ templateId: msg?.templateId ? String(msg.templateId) : undefined,
150
+ dynamicTemplateData: msg?.dynamic_template_data ?? msg?.dynamicTemplateData ?? undefined,
151
+ categories: Array.isArray(msg?.categories) ? msg.categories.map(String) : undefined,
152
+ customArgs: msg?.customArgs ?? msg?.custom_args ?? undefined,
153
+ attachmentsMeta: Array.isArray(msg?.attachments)
154
+ ? msg.attachments.map((a: any) => ({
155
+ filename: a?.filename ? String(a.filename) : undefined,
156
+ type: a?.type ? String(a.type) : undefined,
157
+ size: a?.content ? byteLen(a.content) : undefined, // base64 or string → approximate size
158
+ }))
159
+ : undefined,
160
+ };
161
+
162
+ // If personalizations exist, pull the FIRST one as representative (keeps MVP simple)
163
+ const p0 = Array.isArray(msg?.personalizations) ? msg.personalizations[0] : undefined;
164
+ if (p0) {
165
+ base.to = normalizeAddressList(p0.to) ?? base.to;
166
+ base.cc = normalizeAddressList(p0.cc) ?? base.cc;
167
+ base.bcc = normalizeAddressList(p0.bcc) ?? base.bcc;
168
+ if (!base.subject && p0.subject) base.subject = String(p0.subject);
169
+ // template data can also live inside personalization
170
+ if (!base.dynamicTemplateData && p0.dynamic_template_data) base.dynamicTemplateData = p0.dynamic_template_data;
171
+ if (!base.customArgs && p0.custom_args) base.customArgs = p0.custom_args;
172
+ }
173
+
174
+ return base;
175
+ }
176
+
177
+ function byteLen(content: any): number | undefined {
178
+ try {
179
+ if (typeof content === 'string') return Buffer.byteLength(content, 'utf8');
180
+ if (content && typeof content === 'object' && 'length' in content) return Number((content as any).length);
181
+ } catch {}
182
+ return undefined;
183
+ }
184
+ }
@@ -0,0 +1,281 @@
1
+ // cjs-hook.js
2
+ const fs = require('node:fs');
3
+ const Module = require('node:module');
4
+ const path = require('node:path');
5
+ const babel = require('@babel/core');
6
+ const makeWrap = require('./wrap-plugin');
7
+ const { TraceMap, originalPositionFor } = require('@jridgewell/trace-mapping');
8
+ const { SYM_SRC_FILE, SYM_IS_APP, SYM_BODY_TRACED } = require('./runtime');
9
+
10
+ const CWD = process.cwd().replace(/\\/g, '/');
11
+ const escapeRx = s => s.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
12
+
13
+ // app file check (your existing logic)
14
+ function isAppFile(filename) {
15
+ const f = String(filename || '').replace(/\\/g, '/');
16
+ return f.startsWith(CWD + '/') && !f.includes('/node_modules/');
17
+ }
18
+
19
+ function toPosix(file) {
20
+ return String(file || '').replace(/\\/g, '/');
21
+ }
22
+
23
+ function tagExports(value, filename, seen = new WeakSet(), depth = 0, instrumented = false) {
24
+ if (value == null) return;
25
+ const ty = typeof value;
26
+ if (ty !== 'object' && ty !== 'function') return;
27
+ if (seen.has(value)) return;
28
+ seen.add(value);
29
+
30
+ const isApp = isAppFile(filename);
31
+
32
+ if (typeof value === 'function') {
33
+ try {
34
+ if (!value[SYM_SRC_FILE]) {
35
+ Object.defineProperty(value, SYM_SRC_FILE, { value: filename, configurable: true });
36
+ }
37
+ if (value[SYM_IS_APP] !== isApp) {
38
+ Object.defineProperty(value, SYM_IS_APP, { value: isApp, configurable: true });
39
+ }
40
+ if (instrumented && value.__repro_instrumented !== true) {
41
+ Object.defineProperty(value, '__repro_instrumented', { value: true, configurable: true });
42
+ }
43
+ if (instrumented && value[SYM_BODY_TRACED] !== true) {
44
+ Object.defineProperty(value, SYM_BODY_TRACED, { value: true, configurable: true });
45
+ }
46
+ } catch {}
47
+ const proto = value.prototype;
48
+ if (proto && typeof proto === 'object') {
49
+ for (const k of Object.getOwnPropertyNames(proto)) {
50
+ if (k === 'constructor') continue;
51
+ const d = Object.getOwnPropertyDescriptor(proto, k);
52
+ if (!d) continue;
53
+ if (typeof d.value === 'function') tagExports(d.value, filename, seen, depth + 1, instrumented);
54
+ // also tag accessors
55
+ if (typeof d.get === 'function') tagExports(d.get, filename, seen, depth + 1, instrumented);
56
+ if (typeof d.set === 'function') tagExports(d.set, filename, seen, depth + 1, instrumented);
57
+ }
58
+ }
59
+ }
60
+
61
+ if (typeof value === 'object' && depth < 4) {
62
+ for (const k of Object.getOwnPropertyNames(value)) {
63
+ const d = Object.getOwnPropertyDescriptor(value, k);
64
+ if (!d) continue;
65
+
66
+ if ('value' in d) tagExports(d.value, filename, seen, depth + 1, instrumented);
67
+
68
+ if (typeof d.get === 'function') {
69
+ tagExports(d.get, filename, seen, depth + 1, instrumented);
70
+ try { tagExports(value[k], filename, seen, depth + 1, instrumented); } catch {}
71
+ }
72
+ if (typeof d.set === 'function') {
73
+ tagExports(d.set, filename, seen, depth + 1, instrumented);
74
+ }
75
+ }
76
+ }
77
+ }
78
+
79
+ function installCJS({ include, exclude, parserPlugins } = {}) {
80
+ // default include = project dir; exclude node_modules
81
+ const inc = Array.isArray(include) && include.length
82
+ ? include
83
+ : [ new RegExp('^' + escapeRx(CWD + '/')) ];
84
+ const exc = Array.isArray(exclude) ? exclude : [];
85
+
86
+ const shouldHandle = (f) => {
87
+ const s = String(f || '').replace(/\\/g, '/');
88
+ if (exc.some(rx => rx.test(s))) return false;
89
+ return inc.some(rx => rx.test(s));
90
+ };
91
+
92
+ // ---- Global hook: intercept *all* compiles, regardless of how .ts was compiled ----
93
+ const origCompile = Module.prototype._compile;
94
+ const transformedFiles = new Set();
95
+ Module.prototype._compile = function patchedCompile(code, filename) {
96
+ let out = code;
97
+ let metaFilename = filename;
98
+ let mapOriginalPosition = null;
99
+ let wasInstrumented = false;
100
+ try {
101
+ if (transformedFiles.has(filename)) {
102
+ // Already transformed once; avoid double-transforming code that may already include __repro_call wrappers.
103
+ out = code;
104
+ } else if (shouldHandle(filename) && isAppFile(filename)) {
105
+ try { process.stderr.write(`[trace-debug] compile: ${filename}\n`); } catch {}
106
+ const sourceInfo = getSourceInfo(code, filename);
107
+ if (sourceInfo?.metaFilename) {
108
+ metaFilename = sourceInfo.metaFilename;
109
+ }
110
+ if (typeof sourceInfo?.mapOriginalPosition === 'function') {
111
+ mapOriginalPosition = sourceInfo.mapOriginalPosition;
112
+ }
113
+ // Transform the already-compiled JS (keeps Nest’s decorator metadata intact)
114
+ const res = babel.transformSync(code, {
115
+ filename,
116
+ sourceType: 'unambiguous',
117
+ retainLines: true,
118
+ sourceMaps: 'inline',
119
+ parserOpts: {
120
+ sourceType: 'unambiguous',
121
+ // We’re parsing JS here; TS was compiled by ts-node/Nest.
122
+ plugins: parserPlugins || [
123
+ 'jsx', 'classProperties', 'classPrivateProperties',
124
+ 'classPrivateMethods', 'dynamicImport', 'topLevelAwait',
125
+ 'optionalChaining', 'nullishCoalescingOperator',
126
+ ],
127
+ },
128
+ // only the wrap plugin; do NOT run TS transform here
129
+ plugins: [
130
+ [ makeWrap(metaFilename, {
131
+ mode: 'all',
132
+ wrapGettersSetters: false,
133
+ skipAnonymous: false,
134
+ mapOriginalPosition,
135
+ }) ],
136
+ ],
137
+ compact: false,
138
+ comments: true,
139
+ });
140
+ out = res?.code || code;
141
+ wasInstrumented = !!res?.code;
142
+ transformedFiles.add(filename);
143
+ }
144
+ } catch (err) {
145
+ out = code; // never break the app if transform fails
146
+ try { process.stderr.write(`[trace-debug] compile error: ${filename} :: ${err?.message || err}\n`); } catch {}
147
+ }
148
+
149
+ const ret = origCompile.call(this, out, filename);
150
+
151
+ // Tag exports for origin detection
152
+ try { tagExports(this.exports, metaFilename, new WeakSet(), 0, wasInstrumented); } catch {}
153
+ try { this.__repro_wrapped = true; } catch {}
154
+
155
+ return ret;
156
+ };
157
+
158
+ // keep your load wrapper to tag modules loaded in other ways too
159
+ const _resolveFilename = Module._resolveFilename;
160
+ const _load = Module._load;
161
+ Module._load = function patchedLoad(request, parent, isMain) {
162
+ const filename = (() => {
163
+ try { return _resolveFilename.call(Module, request, parent, isMain); }
164
+ catch { return String(request); } // e.g., 'node:fs'
165
+ })();
166
+ const exp = _load.apply(this, arguments);
167
+ try { tagExports(exp, filename); } catch {}
168
+ return exp;
169
+ };
170
+ }
171
+
172
+ function getSourceInfo(code, filename) {
173
+ try {
174
+ const map = loadSourceMap(code, filename);
175
+ if (!map) return null;
176
+
177
+ const mapFile = map.__mapFile || filename;
178
+ const { sources = [], sourceRoot } = map;
179
+ const resolvedSourceRoot = sourceRoot
180
+ ? resolveSourcePath(sourceRoot, mapFile)
181
+ : '';
182
+
183
+ let metaFilename = null;
184
+ for (const src of sources) {
185
+ if (!src) continue;
186
+ const abs = resolveSourcePath(src, mapFile, resolvedSourceRoot);
187
+ if (!abs) continue;
188
+ metaFilename = toPosix(abs);
189
+ break;
190
+ }
191
+
192
+ const mapper = makeOriginalPositionMapper(map, mapFile, resolvedSourceRoot);
193
+
194
+ return {
195
+ metaFilename,
196
+ mapOriginalPosition: mapper,
197
+ };
198
+ } catch {}
199
+ return null;
200
+ }
201
+
202
+ function makeOriginalPositionMapper(map, mapFile, resolvedSourceRoot) {
203
+ try {
204
+ const traceMap = new TraceMap(map);
205
+ return (line, column = 0) => {
206
+ if (line == null) return null;
207
+ try {
208
+ const original = originalPositionFor(traceMap, { line, column });
209
+ if (!original || original.line == null) return null;
210
+
211
+ const file = original.source
212
+ ? toPosix(resolveSourcePath(original.source, mapFile, resolvedSourceRoot))
213
+ : null;
214
+
215
+ return {
216
+ line: original.line,
217
+ column: original.column ?? 0,
218
+ file,
219
+ };
220
+ } catch {
221
+ return null;
222
+ }
223
+ };
224
+ } catch {
225
+ return null;
226
+ }
227
+ }
228
+
229
+ function resolveSourcePath(sourcePath, relativeTo, rootOverride) {
230
+ try {
231
+ const baseDir = path.dirname(relativeTo);
232
+ const combined = rootOverride
233
+ ? path.resolve(rootOverride, sourcePath)
234
+ : path.resolve(baseDir, sourcePath);
235
+ return combined;
236
+ } catch {
237
+ return null;
238
+ }
239
+ }
240
+
241
+ function loadSourceMap(code, filename) {
242
+ const match = /\/\/[#@]\s*sourceMappingURL=([^\s]+)/.exec(code);
243
+ if (!match) return null;
244
+
245
+ const url = match[1].trim();
246
+ if (!url) return null;
247
+
248
+ if (url.startsWith('data:')) {
249
+ return parseDataUrl(url);
250
+ }
251
+
252
+ const mapPath = path.resolve(path.dirname(filename), url);
253
+ try {
254
+ const text = fs.readFileSync(mapPath, 'utf8');
255
+ const json = JSON.parse(text);
256
+ Object.defineProperty(json, '__mapFile', { value: mapPath });
257
+ return json;
258
+ } catch {
259
+ return null;
260
+ }
261
+ }
262
+
263
+ function parseDataUrl(url) {
264
+ const comma = url.indexOf(',');
265
+ if (comma < 0) return null;
266
+
267
+ const meta = url.slice(5, comma);
268
+ const data = url.slice(comma + 1);
269
+
270
+ try {
271
+ if (/;base64/i.test(meta)) {
272
+ const buf = Buffer.from(data, 'base64');
273
+ return JSON.parse(buf.toString('utf8'));
274
+ }
275
+ return JSON.parse(decodeURIComponent(data));
276
+ } catch {
277
+ return null;
278
+ }
279
+ }
280
+
281
+ module.exports = { installCJS };
@@ -0,0 +1,142 @@
1
+ // dep-hook.js
2
+ 'use strict';
3
+
4
+ const path = require('node:path');
5
+ const shimmer = require('shimmer');
6
+
7
+ const {
8
+ SYM_SKIP_WRAP
9
+ } = require('./runtime');
10
+
11
+ // ---- config / guards ----
12
+ const CWD = process.cwd().replace(/\\/g, '/');
13
+ const isOurFile = (f) => f && f.replace(/\\/g,'/').includes('/omnitrace/') || f.includes('/repro/');
14
+ const isInNodeModules = (f) => f && f.replace(/\\/g,'/').includes('/node_modules/');
15
+
16
+ const SKIP_METHOD_NAMES = new Set([
17
+ // thenables + common promise-ish
18
+ 'then','catch','finally',
19
+ // mongoose query/aggregate “execute” hooks — we log via schema middleware
20
+ 'exec',
21
+ // avoid patching Node’s Symbol-based internals
22
+ Symbol.toStringTag
23
+ ]);
24
+
25
+ // Don’t double-wrap
26
+ function alreadyWrapped(fn) { return !!(fn && fn.__repro_wrapped); }
27
+ function markWrapped(fn) { try { Object.defineProperty(fn, '__repro_wrapped', { value: true }); } catch {} }
28
+
29
+ // our call bridge -> uses your global helper, preserves return value identity
30
+ function wrapFunction(original, label, file, line) {
31
+ if (typeof original !== 'function') return original;
32
+ if (alreadyWrapped(original)) return original;
33
+
34
+ const wrapped = function reproWrapped() {
35
+ // Use the call-site shim to classify app/dep and to safely handle thenables
36
+ return global.__repro_call
37
+ ? global.__repro_call(original, this, Array.from(arguments), file, line, label || original.name || '', false)
38
+ : original.apply(this, arguments);
39
+ };
40
+
41
+ // copy a few common props (name/length are non-writable; don’t force)
42
+ try { wrapped[SYM_SKIP_WRAP] = true; } catch {}
43
+ try { Object.defineProperty(wrapped, '__repro_instrumented', { value: true, configurable: true }); } catch {}
44
+ markWrapped(wrapped);
45
+ return wrapped;
46
+ }
47
+
48
+ function wrapObjectMethods(obj, file) {
49
+ const seen = new WeakSet();
50
+ const maxDepth = 4;
51
+
52
+ const visit = (target, depth = 0) => {
53
+ if (!target || (typeof target !== 'object' && typeof target !== 'function')) return;
54
+ if (seen.has(target)) return;
55
+ if (depth > maxDepth) return;
56
+ seen.add(target);
57
+
58
+ // own props
59
+ for (const k of Object.getOwnPropertyNames(target)) {
60
+ const d = Object.getOwnPropertyDescriptor(target, k);
61
+ if (!d) continue;
62
+ if (d.get || d.set) continue; // never wrap accessors
63
+ if (SKIP_METHOD_NAMES.has(k)) continue;
64
+
65
+ if (typeof d.value === 'function') {
66
+ const v = d.value;
67
+ if (!alreadyWrapped(v)) {
68
+ const w = wrapFunction(v, String(k), file, 0);
69
+ try { shimmer.wrap(target, k, () => w); } catch {
70
+ try { target[k] = w; } catch {}
71
+ }
72
+ }
73
+ continue;
74
+ }
75
+
76
+ if (d.value && (typeof d.value === 'object' || typeof d.value === 'function')) {
77
+ visit(d.value, depth + 1);
78
+ }
79
+ }
80
+
81
+ // class prototype methods (walk depth once)
82
+ const proto = Object.getPrototypeOf(target);
83
+ if (proto && proto !== Object.prototype) {
84
+ visit(proto, depth + 1);
85
+ }
86
+ };
87
+
88
+ visit(obj, 0);
89
+ return obj;
90
+ }
91
+
92
+ function shouldInstrument(filename, moduleName) {
93
+ const f = String(filename || '').replace(/\\/g,'/');
94
+
95
+ if (!f) return false;
96
+ if (isOurFile(f)) return false; // never instrument tracer itself
97
+ if (f.includes('/@babel/')) return false; // avoid babel internals
98
+ if (!isInNodeModules(f) && !f.startsWith(CWD + '/')) { // odd cases
99
+ return false;
100
+ }
101
+ return true;
102
+ }
103
+
104
+ function instrumentExports(exports, filename, moduleName) {
105
+ if (!shouldInstrument(filename, moduleName)) return exports;
106
+
107
+ // Avoid breaking common patterns:
108
+ // - Don’t mutate Mongoose core types (Query/Aggregate) — you already log via schema hooks
109
+ try {
110
+ if (moduleName === 'mongoose' || /[\\/]mongoose[\\/]/.test(filename)) {
111
+ // wrap top-level exported functions only; skip prototype types
112
+ if (exports && typeof exports === 'object') {
113
+ for (const k of Object.getOwnPropertyNames(exports)) {
114
+ const d = Object.getOwnPropertyDescriptor(exports, k);
115
+ if (!d || d.get || d.set) continue;
116
+ if (typeof d.value === 'function' && !SKIP_METHOD_NAMES.has(k)) {
117
+ const w = wrapFunction(d.value, `${moduleName}.${k}`, filename, 0);
118
+ try { shimmer.wrap(exports, k, () => w); } catch {}
119
+ }
120
+ }
121
+ }
122
+ return exports;
123
+ }
124
+ } catch {}
125
+
126
+ // Generic: recursively wrap functions on object exports
127
+ try {
128
+ if (typeof exports === 'function') {
129
+ const wrapped = wrapFunction(exports, moduleName || path.basename(filename), filename, 0);
130
+ try { wrapObjectMethods(wrapped.prototype, filename); } catch {}
131
+ return wrapped;
132
+ }
133
+ if (exports && typeof exports === 'object') {
134
+ const wrappedObj = wrapObjectMethods(exports, filename);
135
+ return wrappedObj;
136
+ }
137
+ } catch {}
138
+
139
+ return exports;
140
+ }
141
+
142
+ module.exports = { instrumentExports };
@@ -0,0 +1,46 @@
1
+ // esm-loader.mjs
2
+ import { readFile } from 'node:fs/promises';
3
+ import { fileURLToPath, pathToFileURL } from 'node:url';
4
+ import * as babel from '@babel/core';
5
+ import tsPlugin from '@babel/plugin-transform-typescript';
6
+ import makeWrap from './wrap-plugin.js';
7
+
8
+ const parserPlugins = [
9
+ 'jsx',
10
+ ['decorators', { version: 'legacy' }], // adjust per your stack
11
+ 'classProperties','classPrivateProperties','classPrivateMethods',
12
+ 'dynamicImport','topLevelAwait','typescript'
13
+ ];
14
+
15
+ // naive include/exclude based on CWD by default
16
+ const CWD = process.cwd().replace(/\\/g,'/');
17
+ const include = [ new RegExp('^' + CWD.replace(/[.*+?^${}()|[\\]\\\\]/g,'\\$&') + '/') ];
18
+ const exclude = [ /node_modules[\\/]/ ];
19
+
20
+ export async function resolve(specifier, context, next) {
21
+ const r = await next(specifier, context);
22
+ return r; // default resolution
23
+ }
24
+
25
+ export async function load(url, context, next) {
26
+ const r = await next(url, context);
27
+ if (r.format !== 'module' && r.format !== 'commonjs') return r;
28
+
29
+ const filename = url.startsWith('file:') ? fileURLToPath(url) : null;
30
+ if (!filename) return r;
31
+
32
+ const s = filename.replace(/\\/g,'/');
33
+ if (exclude.some(rx => rx.test(s)) || !include.some(rx => rx.test(s))) return r;
34
+
35
+ const code = await readFile(filename, 'utf8');
36
+ const out = babel.transformSync(code, {
37
+ filename,
38
+ sourceType: 'unambiguous',
39
+ retainLines: true,
40
+ parserOpts: { sourceType:'unambiguous', plugins: parserPlugins },
41
+ plugins: [[ makeWrap(filename) ], [ tsPlugin, { allowDeclareFields:true } ]],
42
+ sourceMaps: 'inline',
43
+ })?.code || code;
44
+
45
+ return { format: r.format, source: out };
46
+ }