Key Inspector: fix Eval - Hermes rejects async functions in dynamically-eval'd code
This commit is contained in:
parent
4d32246cfc
commit
538c7b8acc
2 changed files with 32 additions and 23 deletions
|
|
@ -39,20 +39,10 @@ function safeStringify(value: any): string {
|
|||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Runs arbitrary code typed into the Eval box, with this repo's own lookup helpers already in
|
||||
* scope - built so an on-device assumption behind a fix (does this property exist, does this call
|
||||
* behave the way decompiled source suggested) can be checked in seconds instead of a full
|
||||
* build/push/update/test cycle. The code runs as the body of an async function, so a bare
|
||||
* expression, a `return ...`, or `await`-ing something all work. Never runs anything on its own -
|
||||
* only what's typed in and manually run here.
|
||||
*/
|
||||
// Android/iOS keyboards commonly have "smart punctuation" that silently swaps straight quotes for
|
||||
// curly ones as you type, even with autoCorrect disabled on some keyboards (GBoard's smart
|
||||
// punctuation is a separate setting) - that turns "foo" into “foo”, which is a different character
|
||||
// to the JS parser and throws "non-terminated string" with no indication why. Normalizing common
|
||||
// substitutions back to their plain-ASCII equivalents before compiling means typing/pasting code on
|
||||
// a phone keyboard doesn't silently produce invalid syntax.
|
||||
// curly ones as you type - that turns "foo" into “foo”, a different character to the JS parser.
|
||||
// Harmless to keep normalizing even though it turned out not to be the actual cause of the errors
|
||||
// seen while building this (see below) - cheap, and still a real footgun on its own.
|
||||
function normalizeSmartPunctuation(code: string): string {
|
||||
return code
|
||||
.replace(/[‘’]/g, "'")
|
||||
|
|
@ -60,6 +50,22 @@ function normalizeSmartPunctuation(code: string): string {
|
|||
.replace(/[–—]/g, "-");
|
||||
}
|
||||
|
||||
/**
|
||||
* Runs arbitrary code typed into the Eval box, with this repo's own lookup helpers already in
|
||||
* scope - built so an on-device assumption behind a fix (does this property exist, does this call
|
||||
* behave the way decompiled source suggested) can be checked in seconds instead of a full
|
||||
* build/push/update/test cycle.
|
||||
*
|
||||
* The dynamically-compiled function body is deliberately NOT `async`. Hermes (RN's JS engine)
|
||||
* precompiles the whole app bundle to bytecode ahead of time, and its runtime `eval`/`new Function`
|
||||
* path only supports a reduced subset of the language - confirmed on-device that it rejects `async`
|
||||
* functions outright ("async functions are unsupported"), even though normal precompiled code
|
||||
* (including this very function) uses async/await freely. So the user's code runs as a plain
|
||||
* synchronous function; if it needs to do something timed/async, it returns a Promise itself
|
||||
* (`return new Promise(resolve => setTimeout(() => resolve(x), 1000))` works fine - only the
|
||||
* `async`/`await` *keywords* are the problem, not Promises as a runtime object) and this function
|
||||
* awaits that result out here, in precompiled code where await is fully supported.
|
||||
*/
|
||||
export async function runEval(rawCode: string): Promise<string> {
|
||||
const code = normalizeSmartPunctuation(rawCode);
|
||||
try {
|
||||
|
|
@ -84,10 +90,10 @@ export async function runEval(rawCode: string): Promise<string> {
|
|||
"ReactNative",
|
||||
"FluxDispatcher",
|
||||
"window",
|
||||
`return (async () => {\n${code}\n})();`
|
||||
`return (function() {\n${code}\n})();`
|
||||
);
|
||||
|
||||
const result = await fn(
|
||||
const result = fn(
|
||||
findByProps,
|
||||
findByName,
|
||||
findByTypeName,
|
||||
|
|
@ -110,7 +116,8 @@ export async function runEval(rawCode: string): Promise<string> {
|
|||
window
|
||||
);
|
||||
|
||||
return safeStringify(result);
|
||||
const resolved = result && typeof result.then === "function" ? await result : result;
|
||||
return safeStringify(resolved);
|
||||
} catch (e) {
|
||||
return `Error: ${e instanceof Error ? `${e.message}\n${e.stack ?? ""}` : String(e)}`;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -82,13 +82,15 @@ function EvalSection() {
|
|||
</Text>
|
||||
)}
|
||||
<Text style={{ marginTop: 8, fontSize: 12.5, opacity: 0.7 }}>
|
||||
Runs as the body of an async function - a bare expression, `return ...`, or
|
||||
`await ...` all work. In scope: findByProps/findByName/findByTypeName/
|
||||
findByStoreName/findByPropsAll/findByNameAll/find, the raw* passive variants
|
||||
(rawFind/rawFindByTypeName/rawFindByProps/rawFindByName/rawFindByStoreName),
|
||||
instead/before/after, getAssetIDByName, React, ReactNative, FluxDispatcher, and
|
||||
window - enough to prototype a real patch here before it goes in a plugin.
|
||||
Nothing runs until you tap the button.
|
||||
Runs as a plain (non-async) function body - a bare expression or `return ...`
|
||||
works, but not the async/await keywords (Hermes doesn't support those in
|
||||
dynamically-eval'd code). For timed/async checks, return a Promise built with a
|
||||
plain function instead of an arrow/async function - it gets awaited for you
|
||||
outside the sandbox. In scope: findByProps/findByName/
|
||||
findByTypeName/findByStoreName/findByPropsAll/findByNameAll/find, the raw*
|
||||
passive variants (rawFind/rawFindByTypeName/rawFindByProps/rawFindByName/
|
||||
rawFindByStoreName), instead/before/after, getAssetIDByName, React, ReactNative,
|
||||
FluxDispatcher, and window. Nothing runs until you tap the button.
|
||||
</Text>
|
||||
</View>
|
||||
</FormSection>
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue