@php-wasm/universal 0.1.37 → 0.1.39

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,172 @@
1
+ import { ErrorEvent } from './error-event-polyfill';
2
+
3
+ type Runtime = {
4
+ asm: Record<string, unknown>;
5
+ lastAsyncifyStackSource?: Error;
6
+ };
7
+
8
+ export class UnhandledRejectionsTarget extends EventTarget {
9
+ listenersCount = 0;
10
+ override addEventListener(type: unknown, callback: unknown): void {
11
+ ++this.listenersCount;
12
+ super.addEventListener(type as string, callback as EventListener);
13
+ }
14
+ override removeEventListener(type: unknown, callback: unknown): void {
15
+ --this.listenersCount;
16
+ super.removeEventListener(type as string, callback as EventListener);
17
+ }
18
+ hasListeners() {
19
+ return this.listenersCount > 0;
20
+ }
21
+ }
22
+
23
+ /**
24
+ * Creates Asyncify errors listener.
25
+ *
26
+ * Emscripten turns Asyncify errors into unhandled rejections by
27
+ * throwing them outside of the context of the original function call.
28
+ *
29
+ * With this listener, we can catch and rethrow them in a proper context,
30
+ * or at least log them in a more readable way.
31
+ *
32
+ * @param runtime
33
+ */
34
+ export function improveWASMErrorReporting(runtime: Runtime) {
35
+ runtime.asm = {
36
+ ...runtime.asm,
37
+ };
38
+ const target = new UnhandledRejectionsTarget();
39
+ for (const key in runtime.asm) {
40
+ if (typeof runtime.asm[key] == 'function') {
41
+ const original = runtime.asm[key] as any;
42
+ runtime.asm[key] = function (...args: any[]) {
43
+ try {
44
+ return original(...args);
45
+ } catch (e) {
46
+ if (!(e instanceof Error)) {
47
+ throw e;
48
+ }
49
+ if ('exitCode' in e && e?.exitCode === 0) {
50
+ return;
51
+ }
52
+ const clearMessage = clarifyErrorMessage(
53
+ e,
54
+ runtime.lastAsyncifyStackSource?.stack
55
+ );
56
+
57
+ if (runtime.lastAsyncifyStackSource) {
58
+ e.cause = runtime.lastAsyncifyStackSource;
59
+ }
60
+
61
+ if (!target.hasListeners()) {
62
+ showCriticalErrorBox(clearMessage);
63
+ throw e;
64
+ }
65
+
66
+ target.dispatchEvent(
67
+ new ErrorEvent('error', {
68
+ error: e,
69
+ message: clearMessage,
70
+ })
71
+ );
72
+ }
73
+ };
74
+ }
75
+ }
76
+ return target;
77
+ }
78
+
79
+ let functionsMaybeMissingFromAsyncify: string[] = [];
80
+ export function getFunctionsMaybeMissingFromAsyncify() {
81
+ return functionsMaybeMissingFromAsyncify;
82
+ }
83
+
84
+ export function clarifyErrorMessage(
85
+ crypticError: Error,
86
+ asyncifyStack?: string
87
+ ) {
88
+ if (crypticError.message === 'unreachable') {
89
+ let betterMessage = UNREACHABLE_ERROR;
90
+ if (!asyncifyStack) {
91
+ betterMessage +=
92
+ `\n\nThis stack trace is lacking. For a better one initialize \n` +
93
+ `the PHP runtime with { debug: true }, e.g. PHPNode.load('8.1', { debug: true }).\n\n`;
94
+ }
95
+ functionsMaybeMissingFromAsyncify = extractPHPFunctionsFromStack(
96
+ asyncifyStack || crypticError.stack || ''
97
+ );
98
+ for (const fn of functionsMaybeMissingFromAsyncify) {
99
+ betterMessage += ` * ${fn}\n`;
100
+ }
101
+ return betterMessage;
102
+ }
103
+ return crypticError.message;
104
+ }
105
+
106
+ const UNREACHABLE_ERROR = `
107
+ "unreachable" WASM instruction executed.
108
+
109
+ The typical reason is a PHP function missing from the ASYNCIFY_ONLY
110
+ list when building PHP.wasm.
111
+
112
+ You will need to file a new issue in the WordPress Playground repository
113
+ and paste this error message there:
114
+
115
+ https://github.com/WordPress/wordpress-playground/issues/new
116
+
117
+ If you're a core developer, the typical fix is to:
118
+
119
+ * Isolate a minimal reproduction of the error
120
+ * Add a reproduction of the error to php-asyncify.spec.ts in the WordPress Playground repository
121
+ * Run 'npm run fix-asyncify'
122
+ * Commit the changes, push to the repo, release updated NPM packages
123
+
124
+ Below is a list of all the PHP functions found in the stack trace to
125
+ help with the minimal reproduction. If they're all already listed in
126
+ the Dockerfile, you'll need to trigger this error again with long stack
127
+ traces enabled. In node.js, you can do it using the --stack-trace-limit=100
128
+ CLI option: \n\n`;
129
+
130
+ // ANSI escape codes for CLI colors and formats
131
+ const redBg = '\x1b[41m';
132
+ const bold = '\x1b[1m';
133
+ const reset = '\x1b[0m';
134
+ const eol = '\x1B[K';
135
+
136
+ let logged = false;
137
+ export function showCriticalErrorBox(message: string) {
138
+ if (logged) {
139
+ return;
140
+ }
141
+ logged = true;
142
+ console.log(`${redBg}\n${eol}\n${bold} WASM ERROR${reset}${redBg}`);
143
+ for (const line of message.split('\n')) {
144
+ console.log(`${eol} ${line} `);
145
+ }
146
+ console.log(`${reset}`);
147
+ }
148
+
149
+ function extractPHPFunctionsFromStack(stack: string) {
150
+ try {
151
+ const names = stack
152
+ .split('\n')
153
+ .slice(1)
154
+ .map((line) => {
155
+ const parts = line.trim().substring('at '.length).split(' ');
156
+ return {
157
+ fn: parts.length >= 2 ? parts[0] : '<unknown>',
158
+ isWasm: line.includes('wasm://'),
159
+ };
160
+ })
161
+ .filter(
162
+ ({ fn, isWasm }) =>
163
+ isWasm &&
164
+ !fn.startsWith('dynCall_') &&
165
+ !fn.startsWith('invoke_')
166
+ )
167
+ .map(({ fn }) => fn);
168
+ return Array.from(new Set(names));
169
+ } catch (err) {
170
+ return [];
171
+ }
172
+ }