Skip to content

Commit e4d17f5

Browse files
authored
feat: add Error.prepareStackTrace handler @W-10270228 (#242)
1 parent 1063ef7 commit e4d17f5

2 files changed

Lines changed: 119 additions & 12 deletions

File tree

packages/near-membrane-base/src/connector.ts

Lines changed: 112 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -6,10 +6,118 @@ const marshallSourceTextInStrictMode = `
66
(function(){
77
'use strict';
88
(${function initializeShadowRealm() {
9-
if (typeof Error.stackTraceLimit === 'number') {
10-
// The default stack trace limit is 10.
11-
// Increasing to 20 as a baby step.
12-
Error.stackTraceLimit *= 2;
9+
// Hook into V8 stack trace API
10+
// https://v8.dev/docs/stack-trace-api
11+
if (typeof Error.prepareStackTrace !== 'function') {
12+
const CallSite = ((): Function | undefined => {
13+
Error.prepareStackTrace = (_error: Error, callSites: NodeJS.CallSite[]) =>
14+
callSites;
15+
const callSites = new Error().stack as string | NodeJS.CallSite[];
16+
delete Error.prepareStackTrace;
17+
return Array.isArray(callSites) && callSites.length > 0
18+
? callSites[0]?.constructor
19+
: undefined;
20+
})();
21+
if (typeof CallSite === 'function') {
22+
// @rollup/plugin-replace replaces `DEV_MODE` references.
23+
const DEV_MODE = true;
24+
const ZERO_WIDTH_JOINER = '\u200D';
25+
const LOCKER_IDENTIFIER_MARKER = `$LWS${ZERO_WIDTH_JOINER}`;
26+
27+
const { toString: ErrorProtoToString } = Error.prototype;
28+
const { apply: ReflectApply, defineProperty: ReflectDefineProperty } = Reflect;
29+
const { endsWith: StringProtoEndsWith, includes: StringProtoIncludes } =
30+
String.prototype;
31+
const {
32+
getEvalOrigin: CallSiteProtoGetEvalOrigin,
33+
getFunctionName: CallSiteProtoGetFunctionName,
34+
toString: CallSiteProtoToString,
35+
} = CallSite.prototype;
36+
37+
const formatStackTrace = function formatStackTrace(
38+
error: Error,
39+
callSites: NodeJS.CallSite[]
40+
): string {
41+
// Based on V8's default stack trace formatting:
42+
// https://chromium.googlesource.com/v8/v8.git/+/refs/heads/main/src/execution/messages.cc#371
43+
let stackTrace = '';
44+
try {
45+
stackTrace = ReflectApply(ErrorProtoToString, error, []);
46+
} catch {
47+
stackTrace = '<error>';
48+
}
49+
let consecutive = false;
50+
for (let i = 0, { length } = callSites; i < length; i += 1) {
51+
const callSite = callSites[i];
52+
const funcName = ReflectApply(CallSiteProtoGetFunctionName, callSite, []);
53+
let isMarked = false;
54+
if (
55+
typeof funcName === 'string' &&
56+
funcName !== 'eval' &&
57+
ReflectApply(StringProtoEndsWith, funcName, [LOCKER_IDENTIFIER_MARKER])
58+
) {
59+
isMarked = true;
60+
}
61+
if (!isMarked) {
62+
const evalOrigin = ReflectApply(
63+
CallSiteProtoGetEvalOrigin,
64+
callSite,
65+
[]
66+
);
67+
if (
68+
typeof evalOrigin === 'string' &&
69+
ReflectApply(StringProtoIncludes, evalOrigin, [
70+
LOCKER_IDENTIFIER_MARKER,
71+
])
72+
) {
73+
isMarked = true;
74+
}
75+
}
76+
// Only write a single LWS entry per consecutive LWS stacks.
77+
if (isMarked) {
78+
if (!consecutive) {
79+
consecutive = true;
80+
stackTrace += '\n at LWS';
81+
}
82+
continue;
83+
} else {
84+
consecutive = false;
85+
}
86+
try {
87+
stackTrace += `\n at ${ReflectApply(
88+
CallSiteProtoToString,
89+
callSite,
90+
[]
91+
)}`;
92+
// eslint-disable-next-line no-empty
93+
} catch {}
94+
}
95+
return stackTrace;
96+
};
97+
// Make Error.prepareStackTrace non-configurable and non-writable.
98+
ReflectDefineProperty(Error, 'prepareStackTrace', {
99+
// @ts-ignore: TS doesn't like __proto__ on property descriptors.
100+
__proto__: null,
101+
enumerable: true,
102+
// Error.prepareStackTrace cannot be a bound or proxy wrapped
103+
// function, so to obscure its source we wrap the call to
104+
// formatStackTrace().
105+
value: function prepareStackTrace(error: Error, callSites: NodeJS.CallSite[]) {
106+
return formatStackTrace(error, callSites);
107+
},
108+
});
109+
// Make Error.stackTraceLimit configurable and writable in DEV_MODE.
110+
ReflectDefineProperty(Error, 'stackTraceLimit', {
111+
// @ts-ignore: TS doesn't like __proto__ on property descriptors.
112+
__proto__: null,
113+
configurable: DEV_MODE,
114+
enumerable: true,
115+
// The default stack trace limit is 10.
116+
// Increasing to 20 for wiggle room of filtered results.
117+
value: 20,
118+
writable: DEV_MODE,
119+
});
120+
}
13121
}
14122
}.toString()})();
15123
return (${createMembraneMarshall.toString()})

packages/near-membrane-base/src/membrane.ts

Lines changed: 7 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -142,6 +142,13 @@ ReflectSetPrototypeOf(SupportFlagsEnum, null);
142142

143143
// istanbul ignore next
144144
export function createMembraneMarshall() {
145+
// @rollup/plugin-replace replaces `DEV_MODE` references.
146+
const DEV_MODE = true;
147+
const LOCKER_LIVE_MARKER_SYMBOL = Symbol.for('@@lockerLiveValue');
148+
const LOCKER_MAGIC_MARKER_SYMBOL = Symbol.for('@@lockerMagicValue');
149+
const { toStringTag: TO_STRING_TAG_SYMBOL } = Symbol;
150+
const UNDEFINED_SYMBOL = Symbol.for('@@membraneUndefinedValue');
151+
145152
const ArrayCtor = Array;
146153
const { isArray: isArrayOrNotOrThrowForRevoked } = Array;
147154
const {
@@ -196,14 +203,6 @@ export function createMembraneMarshall() {
196203
},
197204
}),
198205
globalThis);
199-
200-
// @rollup/plugin-replace replaces `DEV_MODE` references.
201-
const DEV_MODE = true;
202-
const LOCKER_LIVE_MARKER_SYMBOL = Symbol.for('@@lockerLiveValue');
203-
const LOCKER_MAGIC_MARKER_SYMBOL = Symbol.for('@@lockerMagicValue');
204-
const { toStringTag: TO_STRING_TAG_SYMBOL } = Symbol;
205-
const UNDEFINED_SYMBOL = Symbol.for('@@membraneUndefinedValue');
206-
207206
// eslint-disable-next-line no-shadow
208207
enum MarshallSupportFlagsField {
209208
None = 0,

0 commit comments

Comments
 (0)