@php-wasm/universal 0.1.37 → 0.1.40
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/LICENSE +339 -0
- package/package.json +3 -3
- package/src/lib/base-php.ts +81 -14
- package/src/lib/error-event-polyfill.ts +50 -0
- package/src/lib/load-php-runtime.ts +8 -2
- package/src/lib/php-browser.ts +1 -3
- package/src/lib/php-request-handler.ts +0 -2
- package/src/lib/wasm-error-reporting.ts +172 -0
- package/LICENSE.md +0 -750
|
@@ -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
|
+
}
|