From df849df0865778650fb64f9e232e641f06f652f2 Mon Sep 17 00:00:00 2001 From: Garry Tan Date: Mon, 16 Mar 2026 09:43:41 -0500 Subject: [PATCH] feat: support await in $B js and eval commands MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Auto-wrap await expressions in async IIFE context so $B js "await fetch(...)" works without SyntaxError. - hasAwait() strips comments before detection - js: expression wrapping (async()=>(expr))() - eval: smart wrapping — single-line=expression, multi-line=block - 6 new unit tests covering async, false-positive, and return semantics --- browse/src/read-commands.ts | 16 ++++++++++- browse/test/commands.test.ts | 54 ++++++++++++++++++++++++++++++++++++ 2 files changed, 69 insertions(+), 1 deletion(-) diff --git a/browse/src/read-commands.ts b/browse/src/read-commands.ts index 53efec8a..a7d76352 100644 --- a/browse/src/read-commands.ts +++ b/browse/src/read-commands.ts @@ -11,6 +11,12 @@ import type { Page } from 'playwright'; import * as fs from 'fs'; import * as path from 'path'; +/** Detect await keyword, ignoring comments. Accepted risk: await in string literals triggers wrapping (harmless). */ +function hasAwait(code: string): boolean { + const stripped = code.replace(/\/\/.*$/gm, '').replace(/\/\*[\s\S]*?\*\//g, ''); + return /\bawait\b/.test(stripped); +} + // Security: Path validation to prevent path traversal attacks const SAFE_DIRECTORIES = ['/tmp', process.cwd()]; @@ -118,7 +124,8 @@ export async function handleReadCommand( case 'js': { const expr = args[0]; if (!expr) throw new Error('Usage: browse js '); - const result = await page.evaluate(expr); + const wrapped = hasAwait(expr) ? `(async()=>(${expr}))()` : expr; + const result = await page.evaluate(wrapped); return typeof result === 'object' ? JSON.stringify(result, null, 2) : String(result ?? ''); } @@ -128,6 +135,13 @@ export async function handleReadCommand( validateReadPath(filePath); if (!fs.existsSync(filePath)) throw new Error(`File not found: ${filePath}`); const code = fs.readFileSync(filePath, 'utf-8'); + if (hasAwait(code)) { + const trimmed = code.trim(); + const isSingleExpr = trimmed.split('\n').length === 1; + const wrapped = isSingleExpr ? `(async()=>(${trimmed}))()` : `(async()=>{\n${code}\n})()`; + const result = await page.evaluate(wrapped); + return typeof result === 'object' ? JSON.stringify(result, null, 2) : String(result ?? ''); + } const result = await page.evaluate(code); return typeof result === 'object' ? JSON.stringify(result, null, 2) : String(result ?? ''); } diff --git a/browse/test/commands.test.ts b/browse/test/commands.test.ts index a3e201d9..d8aaeab6 100644 --- a/browse/test/commands.test.ts +++ b/browse/test/commands.test.ts @@ -144,6 +144,60 @@ describe('Inspection', () => { expect(obj.b).toBe(2); }); + test('js supports await expressions', async () => { + const result = await handleReadCommand('js', ['await Promise.resolve(42)'], bm); + expect(result).toBe('42'); + }); + + test('js does not false-positive on await substring', async () => { + const result = await handleReadCommand('js', ['(() => { const awaitable = 5; return awaitable })()'], bm); + expect(result).toBe('5'); + }); + + test('eval supports await in single-line file', async () => { + const tmp = '/tmp/eval-await-test.js'; + fs.writeFileSync(tmp, 'await Promise.resolve("hello from eval")'); + try { + const result = await handleReadCommand('eval', [tmp], bm); + expect(result).toBe('hello from eval'); + } finally { + fs.unlinkSync(tmp); + } + }); + + test('eval does not wrap when await is only in a comment', async () => { + const tmp = '/tmp/eval-comment-test.js'; + fs.writeFileSync(tmp, '// no need to await this\ndocument.title'); + try { + const result = await handleReadCommand('eval', [tmp], bm); + expect(result).toBe('Test Page - Basic'); + } finally { + fs.unlinkSync(tmp); + } + }); + + test('eval multi-line with await and explicit return', async () => { + const tmp = '/tmp/eval-multiline-await.js'; + fs.writeFileSync(tmp, 'const data = await Promise.resolve("multi");\nreturn data;'); + try { + const result = await handleReadCommand('eval', [tmp], bm); + expect(result).toBe('multi'); + } finally { + fs.unlinkSync(tmp); + } + }); + + test('eval multi-line with await but no return gives empty string', async () => { + const tmp = '/tmp/eval-multiline-no-return.js'; + fs.writeFileSync(tmp, 'const data = await Promise.resolve("lost");\ndata;'); + try { + const result = await handleReadCommand('eval', [tmp], bm); + expect(result).toBe(''); + } finally { + fs.unlinkSync(tmp); + } + }); + test('css returns computed property', async () => { const result = await handleReadCommand('css', ['h1', 'color'], bm); // Navy color