From 538c7b8accdc00d04e8f0a119b207f5ab5ed7d3c Mon Sep 17 00:00:00 2001 From: everestmcarthur <280397636+everestmcarthur@users.noreply.github.com> Date: Sat, 1 Aug 2026 20:35:24 -0400 Subject: [PATCH] Key Inspector: fix Eval - Hermes rejects async functions in dynamically-eval'd code --- plugins/key-inspector/src/lib/evalTool.ts | 39 +++++++++++++---------- plugins/key-inspector/src/ui/Settings.tsx | 16 ++++++---- 2 files changed, 32 insertions(+), 23 deletions(-) diff --git a/plugins/key-inspector/src/lib/evalTool.ts b/plugins/key-inspector/src/lib/evalTool.ts index 2e20138..df89742 100644 --- a/plugins/key-inspector/src/lib/evalTool.ts +++ b/plugins/key-inspector/src/lib/evalTool.ts @@ -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 { const code = normalizeSmartPunctuation(rawCode); try { @@ -84,10 +90,10 @@ export async function runEval(rawCode: string): Promise { "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 { 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)}`; } diff --git a/plugins/key-inspector/src/ui/Settings.tsx b/plugins/key-inspector/src/ui/Settings.tsx index 0745ebc..f58dcfc 100644 --- a/plugins/key-inspector/src/ui/Settings.tsx +++ b/plugins/key-inspector/src/ui/Settings.tsx @@ -82,13 +82,15 @@ function EvalSection() { )} - 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.