mirror of
https://github.com/garrytan/gstack.git
synced 2026-05-19 02:42:29 +08:00
feat: support await in $B js and eval commands
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
This commit is contained in:
@@ -11,6 +11,12 @@ import type { Page } from 'playwright';
|
|||||||
import * as fs from 'fs';
|
import * as fs from 'fs';
|
||||||
import * as path from 'path';
|
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
|
// Security: Path validation to prevent path traversal attacks
|
||||||
const SAFE_DIRECTORIES = ['/tmp', process.cwd()];
|
const SAFE_DIRECTORIES = ['/tmp', process.cwd()];
|
||||||
|
|
||||||
@@ -118,7 +124,8 @@ export async function handleReadCommand(
|
|||||||
case 'js': {
|
case 'js': {
|
||||||
const expr = args[0];
|
const expr = args[0];
|
||||||
if (!expr) throw new Error('Usage: browse js <expression>');
|
if (!expr) throw new Error('Usage: browse js <expression>');
|
||||||
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 ?? '');
|
return typeof result === 'object' ? JSON.stringify(result, null, 2) : String(result ?? '');
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -128,6 +135,13 @@ export async function handleReadCommand(
|
|||||||
validateReadPath(filePath);
|
validateReadPath(filePath);
|
||||||
if (!fs.existsSync(filePath)) throw new Error(`File not found: ${filePath}`);
|
if (!fs.existsSync(filePath)) throw new Error(`File not found: ${filePath}`);
|
||||||
const code = fs.readFileSync(filePath, 'utf-8');
|
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);
|
const result = await page.evaluate(code);
|
||||||
return typeof result === 'object' ? JSON.stringify(result, null, 2) : String(result ?? '');
|
return typeof result === 'object' ? JSON.stringify(result, null, 2) : String(result ?? '');
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -144,6 +144,60 @@ describe('Inspection', () => {
|
|||||||
expect(obj.b).toBe(2);
|
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 () => {
|
test('css returns computed property', async () => {
|
||||||
const result = await handleReadCommand('css', ['h1', 'color'], bm);
|
const result = await handleReadCommand('css', ['h1', 'color'], bm);
|
||||||
// Navy color
|
// Navy color
|
||||||
|
|||||||
Reference in New Issue
Block a user